Files
UnrealEngine/Engine/Source/Developer/CollisionAnalyzer/Private/SCAQueryDetails.cpp
2025-05-18 13:04:45 +08:00

371 lines
10 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "SCAQueryDetails.h"
#include "Components/PrimitiveComponent.h"
#include "SlateOptMacros.h"
#include "Widgets/Text/STextBlock.h"
#include "Widgets/Layout/SGridPanel.h"
#include "Widgets/Views/SListView.h"
#include "Widgets/Input/SCheckBox.h"
#include "CollisionAnalyzerStyle.h"
#include "SCollisionAnalyzer.h"
#define LOCTEXT_NAMESPACE "SCAQueryDetails"
/** Util to give written explanation for why we missed something */
FText GetReasonForMiss(const UPrimitiveComponent* MissedComp, const FCAQuery* Query)
{
if(MissedComp != NULL && Query != NULL)
{
if(MissedComp->GetOwner() && !MissedComp->GetOwner()->GetActorEnableCollision())
{
return FText::Format(LOCTEXT("MissReasonActorCollisionDisabledFmt", "Owning Actor '{0}' has all collision disabled (SetActorEnableCollision)"), FText::FromString(MissedComp->GetOwner()->GetName()));
}
if(!MissedComp->IsCollisionEnabled())
{
return FText::Format(LOCTEXT("MissReasonComponentCollisionDisabledFmt", "Component '{0}' has CollisionEnabled == NoCollision"), FText::FromString(MissedComp->GetName()));
}
if(MissedComp->GetCollisionResponseToChannel(Query->Channel) == ECR_Ignore)
{
return FText::Format(LOCTEXT("MissReasonComponentIgnoresChannelFmt", "Component '{0}' ignores this channel."), FText::FromString(MissedComp->GetName()));
}
if(Query->ResponseParams.CollisionResponse.GetResponse(MissedComp->GetCollisionObjectType()) == ECR_Ignore)
{
return FText::Format(LOCTEXT("MissReasonQueryIgnoresComponentFmt", "Query ignores Component '{0}' movement channel."), FText::FromString(MissedComp->GetName()));
}
}
return LOCTEXT("MissReasonUnknown", "Unknown");
}
/** Implements a row widget for result list. */
class SHitResultRow : public SMultiColumnTableRow< TSharedPtr<FCAHitInfo> >
{
public:
SLATE_BEGIN_ARGS(SHitResultRow) {}
SLATE_ARGUMENT(TSharedPtr<FCAHitInfo>, Info)
SLATE_ARGUMENT(TSharedPtr<SCAQueryDetails>, OwnerDetailsPtr)
SLATE_END_ARGS()
public:
void Construct(const FArguments& InArgs, const TSharedRef<STableViewBase>& InOwnerTableView)
{
Info = InArgs._Info;
OwnerDetailsPtr = InArgs._OwnerDetailsPtr;
SMultiColumnTableRow< TSharedPtr<FCAHitInfo> >::Construct(FSuperRowType::FArguments(), InOwnerTableView);
}
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
virtual TSharedRef<SWidget> GenerateWidgetForColumn(const FName& ColumnName) override
{
// Get info to apply to all columns (color and tooltip)
FSlateColor ResultColor = FSlateColor::UseForeground();
FText TooltipText = FText::GetEmpty();
if(Info->bMiss)
{
ResultColor = FLinearColor(0.4f,0.4f,0.65f);
TooltipText = FText::Format(LOCTEXT("MissToolTipFmt", "Miss: {0}"), GetReasonForMiss(Info->Result.Component.Get(), OwnerDetailsPtr.Pin()->GetCurrentQuery()));
}
else if(Info->Result.bBlockingHit && Info->Result.bStartPenetrating)
{
ResultColor = FLinearColor(1.f,0.25f,0.25f);
}
// Generate widget for column
if (ColumnName == TEXT("Time"))
{
static const FNumberFormattingOptions TimeNumberFormat = FNumberFormattingOptions()
.SetMinimumFractionalDigits(3)
.SetMaximumFractionalDigits(3);
return SNew(STextBlock)
.ColorAndOpacity( ResultColor )
.ToolTipText( TooltipText )
.Text( FText::AsNumber(Info->Result.Time, &TimeNumberFormat) );
}
else if (ColumnName == TEXT("Type"))
{
FText TypeText = FText::GetEmpty();
if(Info->bMiss)
{
TypeText = LOCTEXT("MissLabel", "Miss");
}
else if(Info->Result.bBlockingHit)
{
TypeText = LOCTEXT("BlockLabel", "Block");
}
else
{
TypeText = LOCTEXT("TouchLabel", "Touch");
}
return SNew(STextBlock)
.ColorAndOpacity( ResultColor )
.ToolTipText( TooltipText )
.Text( TypeText );
}
else if (ColumnName == TEXT("Component"))
{
FText LongName = LOCTEXT("InvalidLabel", "Invalid");
if(Info->Result.Component.IsValid())
{
LongName = FText::FromString(Info->Result.Component.Get()->GetReadableName());
}
return SNew(STextBlock)
.ColorAndOpacity( ResultColor )
.ToolTipText( TooltipText )
.Text( LongName );
}
else if (ColumnName == TEXT("Normal"))
{
return SNew(STextBlock)
.ColorAndOpacity( ResultColor )
.ToolTipText( TooltipText )
.Text( FText::FromString(Info->Result.Normal.ToString()) );
}
return SNullWidget::NullWidget;
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
private:
/** Result to display */
TSharedPtr<FCAHitInfo> Info;
/** Show details of */
TWeakPtr<SCAQueryDetails> OwnerDetailsPtr;
};
//////////////////////////////////////////////////////////////////////////
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SCAQueryDetails::Construct(const FArguments& InArgs, TSharedPtr<SCollisionAnalyzer> OwningAnalyzerWidget)
{
bDisplayQuery = false;
bShowMisses = false;
OwningAnalyzerWidgetPtr = OwningAnalyzerWidget;
ChildSlot
[
SNew(SVerticalBox)
// Top area is info on the trace
+SVerticalBox::Slot()
.AutoHeight()
[
SNew( SBorder )
.BorderImage(FCollisionAnalyzerStyle::Get()->GetBrush("ToolPanel.GroupBorder"))
[
SNew(SHorizontalBox)
// Left is start/end locations
+SHorizontalBox::Slot()
.FillWidth(1)
[
SNew(SGridPanel)
+SGridPanel::Slot(0,0)
.Padding(2)
[
SNew(STextBlock)
.Text(LOCTEXT("QueryStart", "Start:"))
]
+SGridPanel::Slot(1,0)
.Padding(2)
[
SNew(STextBlock)
.Text(this, &SCAQueryDetails::GetStartText)
]
+SGridPanel::Slot(0,1)
.Padding(2)
[
SNew(STextBlock)
.Text(LOCTEXT("QueryEnd", "End:"))
]
+SGridPanel::Slot(1,1)
.Padding(2)
[
SNew(STextBlock)
.Text(this, &SCAQueryDetails::GetEndText)
]
]
// Right has controls
+SHorizontalBox::Slot()
.FillWidth(1)
.VAlign(VAlign_Top)
.Padding(4,0)
[
SNew(SCheckBox)
.OnCheckStateChanged(this, &SCAQueryDetails::OnToggleShowMisses)
.Content()
[
SNew(STextBlock)
.Text(LOCTEXT("ShowMisses", "Show Misses"))
]
]
]
]
// Bottom area is list of hits
+SVerticalBox::Slot()
.FillHeight(1)
[
SNew(SBorder)
.BorderImage(FCollisionAnalyzerStyle::Get()->GetBrush("Menu.Background"))
.Padding(1.0)
[
SAssignNew(ResultListWidget, SListView< TSharedPtr<FCAHitInfo> >)
.ListItemsSource(&ResultList)
.SelectionMode(ESelectionMode::Single)
.OnSelectionChanged(this, &SCAQueryDetails::ResultListSelectionChanged)
.OnGenerateRow(this, &SCAQueryDetails::ResultListGenerateRow)
.HeaderRow(
SNew(SHeaderRow)
+SHeaderRow::Column("Time").DefaultLabel(LOCTEXT("ResultListTimeHeader", "Time")).FillWidth(0.7)
+SHeaderRow::Column("Type").DefaultLabel(LOCTEXT("ResultListTypeHeader", "Type")).FillWidth(0.7)
+SHeaderRow::Column("Component").DefaultLabel(LOCTEXT("ResultListComponentHeader", "Component")).FillWidth(3)
+SHeaderRow::Column("Normal").DefaultLabel(LOCTEXT("ResultListNormalHeader", "Normal")).FillWidth(1.8)
)
]
]
];
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
FText SCAQueryDetails::GetStartText() const
{
return bDisplayQuery ? CurrentQuery.Start.ToText() : FText::GetEmpty();
}
FText SCAQueryDetails::GetEndText() const
{
return bDisplayQuery ? CurrentQuery.End.ToText() : FText::GetEmpty();
}
TSharedRef<ITableRow> SCAQueryDetails::ResultListGenerateRow(TSharedPtr<FCAHitInfo> Info, const TSharedRef<STableViewBase>& OwnerTable)
{
return SNew(SHitResultRow, OwnerTable)
.Info(Info)
.OwnerDetailsPtr(SharedThis(this));
}
void SCAQueryDetails::UpdateDisplayedBox()
{
FCollisionAnalyzer* Analyzer = OwningAnalyzerWidgetPtr.Pin()->Analyzer;
Analyzer->DrawBox = FBox(ForceInit);
if(bDisplayQuery)
{
TArray< TSharedPtr<FCAHitInfo> > SelectedInfos = ResultListWidget->GetSelectedItems();
if(SelectedInfos.Num() > 0)
{
UPrimitiveComponent* HitComp = SelectedInfos[0]->Result.Component.Get();
if(HitComp != NULL)
{
Analyzer->DrawBox = HitComp->Bounds.GetBox();
}
}
}
}
void SCAQueryDetails::ResultListSelectionChanged(TSharedPtr<FCAHitInfo> SelectedInfos, ESelectInfo::Type SelectInfo)
{
UpdateDisplayedBox();
}
void SCAQueryDetails::OnToggleShowMisses(ECheckBoxState InCheckboxState)
{
bShowMisses = (InCheckboxState == ECheckBoxState::Checked);
UpdateResultList();
}
ECheckBoxState SCAQueryDetails::GetShowMissesState() const
{
return bShowMisses ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
}
/** See if an array of results contains a particular component */
static bool ResultsContainComponent(const TArray<FHitResult>& Results, UPrimitiveComponent* Component)
{
for(int32 i=0; i<Results.Num(); i++)
{
if(Results[i].Component.Get() == Component)
{
return true;
}
}
return false;
}
void SCAQueryDetails::UpdateResultList()
{
ResultList.Empty();
UpdateDisplayedBox();
if(bDisplayQuery)
{
// First add actual results
for(int32 i=0; i<CurrentQuery.Results.Num(); i++)
{
ResultList.Add( FCAHitInfo::Make(CurrentQuery.Results[i], false) );
}
// If desired, look for results from our touching query that were not in the real results, and add them
if(bShowMisses)
{
for(int32 i=0; i<CurrentQuery.TouchAllResults.Num(); i++)
{
FHitResult& MissResult = CurrentQuery.TouchAllResults[i];
if(MissResult.Component.IsValid() && !ResultsContainComponent(CurrentQuery.Results, MissResult.Component.Get()))
{
ResultList.Add( FCAHitInfo::Make(MissResult, true) );
}
}
}
// Then sort
struct FCompareFCAHitInfo
{
FORCEINLINE bool operator()( const TSharedPtr<FCAHitInfo> A, const TSharedPtr<FCAHitInfo> B ) const
{
check(A.IsValid());
check(B.IsValid());
return A->Result.Time < B->Result.Time;
}
};
ResultList.Sort( FCompareFCAHitInfo() );
}
// Finally refresh display widget
ResultListWidget->RequestListRefresh();
}
void SCAQueryDetails::SetCurrentQuery(const FCAQuery& NewQuery)
{
bDisplayQuery = true;
CurrentQuery = NewQuery;
UpdateResultList();
}
void SCAQueryDetails::ClearCurrentQuery()
{
bDisplayQuery = false;
ResultList.Empty();
UpdateDisplayedBox();
}
FCAQuery* SCAQueryDetails::GetCurrentQuery()
{
return bDisplayQuery ? &CurrentQuery : NULL;
}
#undef LOCTEXT_NAMESPACE