// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved. #include "DetailCustomizationsPrivatePCH.h" #include "BodyInstanceCustomization.h" #include "ScopedTransaction.h" #include "Editor/Documentation/Public/IDocumentation.h" #include "Engine/CollisionProfile.h" #include "SNumericEntryBox.h" #include "PhysicsEngine/BodySetup.h" #include "ObjectEditorUtils.h" #define LOCTEXT_NAMESPACE "BodyInstanceCustomization" #define RowWidth_Customization 50 //////////////////////////////////////////////////////////////// FBodyInstanceCustomization::FBodyInstanceCustomization() { CollisionProfile = UCollisionProfile::Get(); RefreshCollisionProfiles(); } void FBodyInstanceCustomization::RefreshCollisionProfiles() { int32 NumProfiles = CollisionProfile->GetNumOfProfiles(); // first create profile combo list CollisionProfileComboList.Empty(NumProfiles + 1); // first one is default one CollisionProfileComboList.Add(MakeShareable(new FString(TEXT("Custom...")))); // go through profile and see if it has mine for (int32 ProfileId = 0; ProfileId < NumProfiles; ++ProfileId) { CollisionProfileComboList.Add(MakeShareable(new FString(CollisionProfile->GetProfileByIndex(ProfileId)->Name.ToString()))); } } void FBodyInstanceCustomization::AddCollisionCategory(TSharedRef StructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) { CollisionProfileNameHandle = StructPropertyHandle->GetChildHandle(TEXT("CollisionProfileName")); CollisionEnabledHandle = StructPropertyHandle->GetChildHandle(TEXT("CollisionEnabled")); ObjectTypeHandle = StructPropertyHandle->GetChildHandle(TEXT("ObjectType")); CollisionResponsesHandle = StructPropertyHandle->GetChildHandle(TEXT("CollisionResponses")); check (CollisionProfileNameHandle.IsValid()); check (CollisionEnabledHandle.IsValid()); check (ObjectTypeHandle.IsValid()); // need to find profile name FName ProfileName; TSharedPtr< FString > DisplayName = CollisionProfileComboList[0]; bool bDisplayAdvancedCollisionSettings = true; // if I have valid profile name if (CollisionProfileNameHandle->GetValue(ProfileName) == FPropertyAccess::Result::Success && FBodyInstance::IsValidCollisionProfileName(ProfileName) ) { DisplayName = GetProfileString(ProfileName); bDisplayAdvancedCollisionSettings = false; } const FString PresetsDocLink = TEXT("Shared/Collision"); TSharedPtr ProfileTooltip = IDocumentation::Get()->CreateToolTip(LOCTEXT("SelectCollisionPreset", "Select collision presets. You can set this data in Project settings."), NULL, PresetsDocLink, TEXT("PresetDetail")); IDetailGroup& CollisionGroup = StructBuilder.AddChildGroup( TEXT("Collision"), LOCTEXT("CollisionPresetsLabel", "Collision Presets") ); CollisionGroup.HeaderRow() .NameContent() [ SNew(STextBlock) .Text(LOCTEXT("CollisionPresetsLabel", "Collision Presets")) .ToolTip(ProfileTooltip) .Font( IDetailLayoutBuilder::GetDetailFont() ) ] .ValueContent() [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .VAlign(VAlign_Center) [ SAssignNew(CollsionProfileComboBox, SComboBox< TSharedPtr >) .OptionsSource(&CollisionProfileComboList) .OnGenerateWidget(this, &FBodyInstanceCustomization::MakeCollisionProfileComboWidget) .OnSelectionChanged(this, &FBodyInstanceCustomization::OnCollisionProfileChanged, &CollisionGroup ) .OnComboBoxOpening(this, &FBodyInstanceCustomization::OnCollisionProfileComboOpening) .InitiallySelectedItem(DisplayName) .IsEnabled( FSlateApplication::Get().GetNormalExecutionAttribute() ) .ContentPadding(2) .Content() [ SNew( STextBlock ) .Text(this, &FBodyInstanceCustomization::GetCollisionProfileComboBoxContent) .Font( IDetailLayoutBuilder::GetDetailFont() ) .ToolTipText(this, &FBodyInstanceCustomization::GetCollisionProfileComboBoxToolTip) ] ] +SHorizontalBox::Slot() .VAlign(VAlign_Center) .Padding( 2.0f ) .AutoWidth() [ SNew(SButton) .OnClicked(this, &FBodyInstanceCustomization::SetToDefaultProfile) .Visibility(this, &FBodyInstanceCustomization::ShouldShowResetToDefaultProfile) .ContentPadding(0.f) .ToolTipText(LOCTEXT("ResetToDefaultToolTip", "Reset to Default")) .ButtonStyle( FEditorStyle::Get(), "NoBorder" ) .Content() [ SNew(SImage) .Image( FEditorStyle::GetBrush("PropertyWindow.DiffersFromDefault") ) ] ] ]; CollisionGroup.ToggleExpansion(bDisplayAdvancedCollisionSettings); // now create custom set up CreateCustomCollisionSetup( StructPropertyHandle, CollisionGroup ); } BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION void FBodyInstanceCustomization::CustomizeChildren( TSharedRef StructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils ) { // copy all bodyinstances I'm accessing right now TArray StructPtrs; StructPropertyHandle->AccessRawData(StructPtrs); check(StructPtrs.Num() != 0); BodyInstances.AddUninitialized(StructPtrs.Num()); for (auto Iter = StructPtrs.CreateIterator(); Iter; ++Iter) { check(*Iter); BodyInstances[Iter.GetIndex()] = (FBodyInstance*)(*Iter); } AddCollisionCategory(StructPropertyHandle, StructBuilder, StructCustomizationUtils); } END_SLATE_FUNCTION_BUILD_OPTIMIZATION int32 FBodyInstanceCustomization::InitializeObjectTypeComboList() { ObjectTypeComboList.Empty(); ObjectTypeValues.Empty(); UEnum * Enum = FindObject(ANY_PACKAGE, TEXT("ECollisionChannel"), true); const FString KeyName = TEXT("DisplayName"); const FString QueryType = TEXT("TraceQuery"); int32 NumEnum = Enum->NumEnums(); int32 Selected = 0; uint8 ObjectTypeIndex = 0; if (ObjectTypeHandle->GetValue(ObjectTypeIndex) != FPropertyAccess::Result::Success) { ObjectTypeIndex = 0; // if multi, just let it be 0 } // go through enum and fill up the list for (int32 EnumIndex = 0; EnumIndex < NumEnum; ++EnumIndex) { // make sure the enum entry is object channel FString MetaData = Enum->GetMetaData(*QueryType, EnumIndex); // if query type is object, we allow it to be on movement channel if (MetaData.Len() == 0 || MetaData[0] == '0') { MetaData = Enum->GetMetaData(*KeyName, EnumIndex); if ( MetaData.Len() > 0 ) { int32 NewIndex = ObjectTypeComboList.Add( MakeShareable( new FString (MetaData) )); // @todo: I don't think this would work well if we customize entry, but I don't think we can do that yet // i.e. enum a { a1=5, a2=6 } ObjectTypeValues.Add((ECollisionChannel)EnumIndex); // this solution poses problem when the item was saved with ALREADY INVALID movement channel // that will automatically select 0, but I think that is the right solution if (ObjectTypeIndex == EnumIndex) { Selected = NewIndex; } } } } // it can't be zero. If so you need to fix it check ( ObjectTypeComboList.Num() > 0 ); return Selected; } TSharedPtr FBodyInstanceCustomization::GetProfileString(FName ProfileName) const { FString ProfileNameString = ProfileName.ToString(); // go through profile and see if it has mine int32 NumProfiles = CollisionProfile->GetNumOfProfiles(); // refresh collision count if( NumProfiles+1==CollisionProfileComboList.Num() ) { for(int32 ProfileId = 0; ProfileId < NumProfiles; ++ProfileId) { if(*CollisionProfileComboList[ProfileId+1].Get() == ProfileNameString) { return CollisionProfileComboList[ProfileId+1]; } } } return CollisionProfileComboList[0]; } // filter through find valid index of enum values matching each item // this needs refresh when displayname of the enum has change // which can happen when we have engine project settings in place working void FBodyInstanceCustomization::UpdateValidCollisionChannels() { // find the enum UEnum * Enum = FindObject(ANY_PACKAGE, TEXT("ECollisionChannel"), true); // we need this Enum check (Enum); const FString KeyName = TEXT("DisplayName"); const FString TraceType = TEXT("TraceQuery"); // need to initialize displaynames separate int32 NumEnum = Enum->NumEnums(); ValidCollisionChannels.Empty(NumEnum); // first go through enum entry, and add suffix to displaynames for ( int32 EnumIndex=0; EnumIndexGetMetaData(*KeyName, EnumIndex); if ( MetaData.Len() > 0 ) { FCollisionChannelInfo Info; Info.DisplayName = MetaData; Info.CollisionChannel = (ECollisionChannel)EnumIndex; if (Enum->GetMetaData(*TraceType, EnumIndex) == TEXT("1")) { Info.TraceType = true; } else { Info.TraceType = false; } ValidCollisionChannels.Add(Info); } } } BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION void FBodyInstanceCustomization::CreateCustomCollisionSetup( TSharedRef StructPropertyHandle, class IDetailGroup& CollisionGroup ) { UpdateValidCollisionChannels(); if (ValidCollisionChannels.Num() == 0) { return; } int32 TotalNumChildren = ValidCollisionChannels.Num(); TAttribute CustomCollisionEnabled( this, &FBodyInstanceCustomization::ShouldEnableCustomCollisionSetup ); TAttribute CustomCollisionVisibility(this, &FBodyInstanceCustomization::ShouldShowCustomCollisionSetup); // initialize ObjectTypeComboList // we only display things that has "DisplayName" int32 IndexSelected = InitializeObjectTypeComboList(); CollisionGroup.AddPropertyRow(CollisionEnabledHandle.ToSharedRef()) .IsEnabled(CustomCollisionEnabled) .Visibility(CustomCollisionVisibility); if (!StructPropertyHandle->GetProperty()->GetBoolMetaData(TEXT("HideObjectType"))) { CollisionGroup.AddWidgetRow() .IsEnabled(CustomCollisionEnabled) .Visibility(CustomCollisionVisibility) .NameContent() [ ObjectTypeHandle->CreatePropertyNameWidget() ] .ValueContent() [ SAssignNew(ObjectTypeComboBox, SComboBox>) .OptionsSource(&ObjectTypeComboList) .OnGenerateWidget(this, &FBodyInstanceCustomization::MakeObjectTypeComboWidget) .OnSelectionChanged(this, &FBodyInstanceCustomization::OnObjectTypeChanged) .InitiallySelectedItem(ObjectTypeComboList[IndexSelected]) .IsEnabled(FSlateApplication::Get().GetNormalExecutionAttribute()) .ContentPadding(2) .Content() [ SNew(STextBlock) .Text(this, &FBodyInstanceCustomization::GetObjectTypeComboBoxContent) .Font(IDetailLayoutBuilder::GetDetailFont()) ] ]; } // Add Title CollisionGroup.AddWidgetRow() .IsEnabled(CustomCollisionEnabled) .Visibility(CustomCollisionVisibility) .ValueContent() .MaxDesiredWidth(0) .MinDesiredWidth(0) [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() [ SNew(SBox) .WidthOverride(RowWidth_Customization) .HAlign( HAlign_Left ) .Content() [ SNew(STextBlock) .Text(LOCTEXT("IgnoreCollisionLabel", "Ignore")) .Font( IDetailLayoutBuilder::GetDetailFontBold() ) ] ] +SHorizontalBox::Slot() .AutoWidth() [ SNew(SBox) .HAlign( HAlign_Left ) .WidthOverride(RowWidth_Customization) .Content() [ SNew(STextBlock) .Text(LOCTEXT("OverlapCollisionLabel", "Overlap")) .Font( IDetailLayoutBuilder::GetDetailFontBold() ) ] ] +SHorizontalBox::Slot() .AutoWidth() [ SNew(STextBlock) .Text(LOCTEXT("BlockCollisionLabel", "Block")) .Font( IDetailLayoutBuilder::GetDetailFontBold() ) ] ]; // Add All check box CollisionGroup.AddWidgetRow() .IsEnabled(CustomCollisionEnabled) .Visibility(CustomCollisionVisibility) .NameContent() [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .Padding( 2.0f ) .VAlign(VAlign_Center) .AutoWidth() [ SNew( STextBlock ) .Text(LOCTEXT("CollisionResponsesLabel", "Collision Responses")) .Font( IDetailLayoutBuilder::GetDetailFontBold() ) .ToolTipText(LOCTEXT("CollsionResponse_ToolTip", "When trace by channel, this information will be used for filtering.")) ] +SHorizontalBox::Slot() .HAlign(HAlign_Left) .VAlign(VAlign_Center) [ IDocumentation::Get()->CreateAnchor( TEXT("Engine/Physics/Collision") ) ] ] .ValueContent() [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() [ SNew(SBox) .WidthOverride(RowWidth_Customization) .Content() [ SNew(SCheckBox) .OnCheckStateChanged( this, &FBodyInstanceCustomization::OnAllCollisionChannelChanged, ECR_Ignore ) .IsChecked( this, &FBodyInstanceCustomization::IsAllCollisionChannelChecked, ECR_Ignore ) ] ] +SHorizontalBox::Slot() .AutoWidth() [ SNew(SBox) .WidthOverride(RowWidth_Customization) .Content() [ SNew(SCheckBox) .OnCheckStateChanged( this, &FBodyInstanceCustomization::OnAllCollisionChannelChanged, ECR_Overlap ) .IsChecked( this, &FBodyInstanceCustomization::IsAllCollisionChannelChecked, ECR_Overlap ) ] ] +SHorizontalBox::Slot() .AutoWidth() [ SNew(SBox) .WidthOverride(RowWidth_Customization) .Content() [ SNew(SCheckBox) .OnCheckStateChanged( this, &FBodyInstanceCustomization::OnAllCollisionChannelChanged, ECR_Block ) .IsChecked( this, &FBodyInstanceCustomization::IsAllCollisionChannelChecked, ECR_Block ) ] ] ]; // add header // Add Title CollisionGroup.AddWidgetRow() .IsEnabled(CustomCollisionEnabled) .Visibility(CustomCollisionVisibility) .NameContent() [ SNew(SBox) .Padding(FMargin(10, 0)) .Content() [ SNew(STextBlock) .Text(LOCTEXT("CollisionTraceResponsesLabel", "Trace Responses")) .Font(IDetailLayoutBuilder::GetDetailFontBold()) ] ]; // each channel set up FName MetaData(TEXT("DisplayName")); // Add option for each channel - first do trace for ( int32 Index=0; Index FBodyInstanceCustomization::MakeObjectTypeComboWidget( TSharedPtr InItem ) { return SNew(STextBlock) .Text( FText::FromString(*InItem) ) .Font( IDetailLayoutBuilder::GetDetailFont() ); } void FBodyInstanceCustomization::OnObjectTypeChanged( TSharedPtr NewSelection, ESelectInfo::Type SelectInfo ) { // if it's set from code, we did that on purpose if (SelectInfo != ESelectInfo::Direct) { FString NewValue = *NewSelection.Get(); uint8 NewEnumVal = ECC_WorldStatic; // find index of NewValue for (auto Iter = ObjectTypeComboList.CreateConstIterator(); Iter; ++Iter) { // if value is same if (*(Iter->Get()) == NewValue) { NewEnumVal = ObjectTypeValues[Iter.GetIndex()]; } } ensure(ObjectTypeHandle->SetValue(NewEnumVal) == FPropertyAccess::Result::Success); } } FText FBodyInstanceCustomization::GetObjectTypeComboBoxContent() const { FName ObjectTypeName; if (ObjectTypeHandle->GetValue(ObjectTypeName) == FPropertyAccess::Result::MultipleValues) { return LOCTEXT("MultipleValues", "Multiple Values"); } return FText::FromString(*ObjectTypeComboBox.Get()->GetSelectedItem().Get()); } TSharedRef FBodyInstanceCustomization::MakeCollisionProfileComboWidget(TSharedPtr InItem) { FString ProfileMessage; FCollisionResponseTemplate ProfileData; if (CollisionProfile->GetProfileTemplate(FName(**InItem), ProfileData)) { ProfileMessage = ProfileData.HelpMessage; } return SNew(STextBlock) .Text(FText::FromString(*InItem)) .ToolTipText(FText::FromString(ProfileMessage)) .Font(IDetailLayoutBuilder::GetDetailFont()); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // NOTE! I have a lot of ensure to make sure it's set correctly // in case for if type changes or any set up changes, this won't work, but ensure will remind you that! :) ///////////////////////////////////////////////////////////////////////////////////////////////////////////// void FBodyInstanceCustomization::OnCollisionProfileComboOpening() { FName ProfileName; if (CollisionProfileNameHandle->GetValue(ProfileName) != FPropertyAccess::Result::MultipleValues) { TSharedPtr ComboStringPtr = GetProfileString(ProfileName); if( ComboStringPtr.IsValid() ) { CollsionProfileComboBox->SetSelectedItem(ComboStringPtr); } } } void FBodyInstanceCustomization::OnCollisionProfileChanged( TSharedPtr NewSelection, ESelectInfo::Type SelectInfo, IDetailGroup* CollisionGroup ) { // if it's set from code, we did that on purpose if (SelectInfo != ESelectInfo::Direct) { FString NewValue = *NewSelection.Get(); int32 NumProfiles = CollisionProfile->GetNumOfProfiles(); for (int32 ProfileId = 0; ProfileId < NumProfiles; ++ProfileId) { const FCollisionResponseTemplate* CurProfile = CollisionProfile->GetProfileByIndex(ProfileId); if ( NewValue == CurProfile->Name.ToString() ) { // trigget transaction before UpdateCollisionProfile const FScopedTransaction Transaction( LOCTEXT( "ChangeCollisionProfile", "Change Collision Profile" ) ); // set profile set up ensure ( CollisionProfileNameHandle->SetValue(NewValue) == FPropertyAccess::Result::Success ); UpdateCollisionProfile(); return; } } if( NewSelection == CollisionProfileComboList[0] ) { // Force expansion when the user chooses the selected item CollisionGroup->ToggleExpansion( true ); } // if none of them found, clear it FName Name=UCollisionProfile::CustomCollisionProfileName; ensure ( CollisionProfileNameHandle->SetValue(Name) == FPropertyAccess::Result::Success ); } } void FBodyInstanceCustomization::UpdateCollisionProfile() { FName ProfileName; // if I have valid profile name if (CollisionProfileNameHandle->GetValue(ProfileName) == FPropertyAccess::Result::Success && FBodyInstance::IsValidCollisionProfileName(ProfileName) ) { int32 NumProfiles = CollisionProfile->GetNumOfProfiles(); for (int32 ProfileId = 0; ProfileId < NumProfiles; ++ProfileId) { // find the profile const FCollisionResponseTemplate* CurProfile = CollisionProfile->GetProfileByIndex(ProfileId); if (ProfileName == CurProfile->Name) { // set the profile set up ensure(CollisionEnabledHandle->SetValue((uint8)CurProfile->CollisionEnabled) == FPropertyAccess::Result::Success); ensure(ObjectTypeHandle->SetValue((uint8)CurProfile->ObjectType) == FPropertyAccess::Result::Success); SetCollisionResponseContainer(CurProfile->ResponseToChannels); // now update combo box CollsionProfileComboBox.Get()->SetSelectedItem(CollisionProfileComboList[ProfileId+1]); if (ObjectTypeComboBox.IsValid()) { for (auto Iter = ObjectTypeValues.CreateConstIterator(); Iter; ++Iter) { if (*Iter == CurProfile->ObjectType) { ObjectTypeComboBox.Get()->SetSelectedItem(ObjectTypeComboList[Iter.GetIndex()]); break; } } } return; } } } CollsionProfileComboBox.Get()->SetSelectedItem(CollisionProfileComboList[0]); } FReply FBodyInstanceCustomization::SetToDefaultProfile() { // trigger transaction before UpdateCollisionProfile const FScopedTransaction Transaction( LOCTEXT( "ResetCollisionProfile", "Reset Collision Profile" ) ); CollisionProfileNameHandle.Get()->ResetToDefault(); UpdateCollisionProfile(); return FReply::Handled(); } EVisibility FBodyInstanceCustomization::ShouldShowResetToDefaultProfile() const { if (CollisionProfileNameHandle.Get()->DiffersFromDefault()) { return EVisibility::Visible; } return EVisibility::Hidden; } FReply FBodyInstanceCustomization::SetToDefaultResponse(int32 ValidIndex) { if ( ValidCollisionChannels.IsValidIndex(ValidIndex) ) { const FScopedTransaction Transaction( LOCTEXT( "ResetCollisionResponse", "Reset Collision Response" ) ); const ECollisionResponse DefaultResponse = FCollisionResponseContainer::GetDefaultResponseContainer().GetResponse(ValidCollisionChannels[ValidIndex].CollisionChannel); SetResponse(ValidIndex, DefaultResponse); return FReply::Handled(); } return FReply::Unhandled(); } EVisibility FBodyInstanceCustomization::ShouldShowResetToDefaultResponse(int32 ValidIndex) const { if ( ValidCollisionChannels.IsValidIndex(ValidIndex) ) { const ECollisionResponse DefaultResponse = FCollisionResponseContainer::GetDefaultResponseContainer().GetResponse(ValidCollisionChannels[ValidIndex].CollisionChannel); if (IsCollisionChannelChecked(ValidIndex, DefaultResponse) != ECheckBoxState::Checked) { return EVisibility::Visible; } } return EVisibility::Hidden; } bool FBodyInstanceCustomization::ShouldEnableCustomCollisionSetup() const { FName ProfileName; if (CollisionProfileNameHandle->GetValue(ProfileName) == FPropertyAccess::Result::Success && FBodyInstance::IsValidCollisionProfileName(ProfileName) == false) { return true; } return false; } EVisibility FBodyInstanceCustomization::ShouldShowCustomCollisionSetup() const { return EVisibility::Visible;//return (bDisplayAdvancedCollisionSettings)? EVisibility::Visible : EVisibility::Hidden; } FText FBodyInstanceCustomization::GetCollisionProfileComboBoxContent() const { FName ProfileName; if (CollisionProfileNameHandle->GetValue(ProfileName) == FPropertyAccess::Result::MultipleValues) { return LOCTEXT("MultipleValues", "Multiple Values"); } return FText::FromString(*GetProfileString(ProfileName).Get()); } FText FBodyInstanceCustomization::GetCollisionProfileComboBoxToolTip() const { FName ProfileName; if (CollisionProfileNameHandle->GetValue(ProfileName) == FPropertyAccess::Result::Success) { FCollisionResponseTemplate ProfileData; if ( CollisionProfile->GetProfileTemplate(ProfileName, ProfileData) ) { return FText::FromString(ProfileData.HelpMessage); } return FText::GetEmpty(); } return LOCTEXT("MultipleValues", "Multiple Values"); } void FBodyInstanceCustomization::OnCollisionChannelChanged(ECheckBoxState InNewValue, int32 ValidIndex, ECollisionResponse InCollisionResponse) { if ( ValidCollisionChannels.IsValidIndex(ValidIndex) ) { SetResponse(ValidIndex, InCollisionResponse); } } void FBodyInstanceCustomization::SetResponse(int32 ValidIndex, ECollisionResponse InCollisionResponse) { const FScopedTransaction Transaction( LOCTEXT( "ChangeIndividualChannel", "Change Individual Channel" ) ); CollisionResponsesHandle->NotifyPreChange(); for (auto Iter=BodyInstances.CreateIterator(); Iter; ++Iter) { FBodyInstance* BodyInstance = *Iter; BodyInstance->CollisionResponses.SetResponse(ValidCollisionChannels[ValidIndex].CollisionChannel, InCollisionResponse); } CollisionResponsesHandle->NotifyPostChange(); } ECheckBoxState FBodyInstanceCustomization::IsCollisionChannelChecked( int32 ValidIndex, ECollisionResponse InCollisionResponse) const { TArray CollisionResponses; if ( ValidCollisionChannels.IsValidIndex(ValidIndex) ) { for (auto Iter=BodyInstances.CreateConstIterator(); Iter; ++Iter) { FBodyInstance* BodyInstance = *Iter; CollisionResponses.AddUnique(BodyInstance->CollisionResponses.GetResponse(ValidCollisionChannels[ValidIndex].CollisionChannel)); } if (CollisionResponses.Num() == 1) { if (CollisionResponses[0] == InCollisionResponse) { return ECheckBoxState::Checked; } else { return ECheckBoxState::Unchecked; } } else if (CollisionResponses.Contains(InCollisionResponse)) { return ECheckBoxState::Undetermined; } // if it didn't contain and it's not found, return Unchecked return ECheckBoxState::Unchecked; } return ECheckBoxState::Undetermined; } void FBodyInstanceCustomization::OnAllCollisionChannelChanged(ECheckBoxState InNewValue, ECollisionResponse InCollisionResponse) { FCollisionResponseContainer NewContainer; NewContainer.SetAllChannels(InCollisionResponse); SetCollisionResponseContainer(NewContainer); } ECheckBoxState FBodyInstanceCustomization::IsAllCollisionChannelChecked(ECollisionResponse InCollisionResponse) const { ECheckBoxState State = ECheckBoxState::Undetermined; uint32 TotalNumChildren = ValidCollisionChannels.Num(); if (TotalNumChildren >= 1) { State = IsCollisionChannelChecked(0, InCollisionResponse); for (uint32 Index = 1; Index < TotalNumChildren; ++Index) { if (State != IsCollisionChannelChecked(Index, InCollisionResponse)) { State = ECheckBoxState::Undetermined; break; } } } return State; } void FBodyInstanceCustomization::SetCollisionResponseContainer(const FCollisionResponseContainer& ResponseContainer) { // trigget transaction before UpdateCollisionProfile const FScopedTransaction Transaction( LOCTEXT( "Collision", "Collision Channel Changes" ) ); CollisionResponsesHandle->NotifyPreChange(); uint32 TotalNumChildren = ValidCollisionChannels.Num(); // only go through valid channels for (uint32 Index = 0; Index < TotalNumChildren; ++Index) { ECollisionChannel Channel = ValidCollisionChannels[Index].CollisionChannel; ECollisionResponse Response = ResponseContainer.GetResponse(Channel); // iterate through bodyinstance and fix it for (FBodyInstance* BodyInstance : BodyInstances) { BodyInstance->CollisionResponses.SetResponse(Channel, Response); } } CollisionResponsesHandle->NotifyPostChange(); } FBodyInstanceCustomizationHelper::FBodyInstanceCustomizationHelper(const TArray>& InObjectsCustomized) : ObjectsCustomized(InObjectsCustomized) { } void FBodyInstanceCustomizationHelper::UpdateFilters() { bDisplayMass = true; bDisplayConstraints = true; bDisplayEnablePhysics = true; for (int32 i = 0; i < ObjectsCustomized.Num(); ++i) { if (ObjectsCustomized[i].IsValid()) { if(ObjectsCustomized[i]->IsA(UDestructibleComponent::StaticClass())) { bDisplayMass = false; bDisplayConstraints = false; } else { if(ObjectsCustomized[i]->IsA(UBodySetup::StaticClass())) { bDisplayEnablePhysics = false; bDisplayConstraints = false; } } } } } void FBodyInstanceCustomizationHelper::CustomizeDetails( IDetailLayoutBuilder& DetailBuilder, TSharedRef BodyInstanceHandler) { if( BodyInstanceHandler->IsValidHandle() ) { UpdateFilters(); IDetailCategoryBuilder& PhysicsCategory = DetailBuilder.EditCategory("Physics"); TSharedRef PhysicsEnable = BodyInstanceHandler->GetChildHandle(GET_MEMBER_NAME_CHECKED(FBodyInstance, bSimulatePhysics)).ToSharedRef(); if(bDisplayEnablePhysics) { PhysicsCategory.AddProperty(PhysicsEnable).EditCondition(TAttribute(this, &FBodyInstanceCustomizationHelper::IsSimulatePhysicsEditable), NULL); } else { PhysicsEnable->MarkHiddenByCustomization(); } AddMassInKg(PhysicsCategory, BodyInstanceHandler); PhysicsCategory.AddProperty(BodyInstanceHandler->GetChildHandle(GET_MEMBER_NAME_CHECKED(FBodyInstance, LinearDamping))); PhysicsCategory.AddProperty(BodyInstanceHandler->GetChildHandle(GET_MEMBER_NAME_CHECKED(FBodyInstance, AngularDamping))); PhysicsCategory.AddProperty(BodyInstanceHandler->GetChildHandle(GET_MEMBER_NAME_CHECKED(FBodyInstance, bEnableGravity))); AddBodyConstraint(PhysicsCategory, BodyInstanceHandler); //ADVANCED PhysicsCategory.AddProperty(BodyInstanceHandler->GetChildHandle(GET_MEMBER_NAME_CHECKED(FBodyInstance, bAutoWeld))) .Visibility(TAttribute(this, &FBodyInstanceCustomizationHelper::IsAutoWeldVisible)) .EditCondition(TAttribute(this, &FBodyInstanceCustomizationHelper::IsAutoWeldEditable), NULL); PhysicsCategory.AddProperty(BodyInstanceHandler->GetChildHandle(GET_MEMBER_NAME_CHECKED(FBodyInstance, bStartAwake))); PhysicsCategory.AddProperty(BodyInstanceHandler->GetChildHandle(GET_MEMBER_NAME_CHECKED(FBodyInstance, COMNudge))); PhysicsCategory.AddProperty(BodyInstanceHandler->GetChildHandle(GET_MEMBER_NAME_CHECKED(FBodyInstance, MassScale))); AddMaxAngularVelocity(PhysicsCategory, BodyInstanceHandler); TSharedRef AsyncEnabled = BodyInstanceHandler->GetChildHandle(GET_MEMBER_NAME_CHECKED(FBodyInstance, bUseAsyncScene)).ToSharedRef(); PhysicsCategory.AddProperty(AsyncEnabled).EditCondition(TAttribute(this, &FBodyInstanceCustomizationHelper::IsUseAsyncEditable), NULL); //Add the rest uint32 NumChildren = 0; BodyInstanceHandler->GetNumChildren(NumChildren); for(uint32 ChildIdx = 0; ChildIdx < NumChildren; ++ChildIdx) { TSharedPtr ChildProp = BodyInstanceHandler->GetChildHandle(ChildIdx); FString Category = FObjectEditorUtils::GetCategory(ChildProp->GetProperty()); if(ChildProp->IsCustomized() == false && Category == TEXT("Physics")) //add the rest of the physics properties { PhysicsCategory.AddProperty(ChildProp); } } } } bool FBodyInstanceCustomizationHelper::IsSimulatePhysicsEditable() const { // Check whether to enable editing of bSimulatePhysics - this will happen if all objects are UPrimitiveComponents & have collision geometry. bool bEnableSimulatePhysics = ObjectsCustomized.Num() > 0; for (TWeakObjectPtr CustomizedObject : ObjectsCustomized) { if (UPrimitiveComponent* PrimitiveComponent = Cast(CustomizedObject.Get())) { if (!PrimitiveComponent->CanEditSimulatePhysics()) { bEnableSimulatePhysics = false; break; } } } return bEnableSimulatePhysics; } bool FBodyInstanceCustomizationHelper::IsUseAsyncEditable() const { // Check whether to enable editing of bUseAsyncScene - this will happen if all objects are movable and the project uses an AsyncScene if (!UPhysicsSettings::Get()->bEnableAsyncScene) { return false; } bool bEnableUseAsyncScene = ObjectsCustomized.Num() > 0; for (auto ObjectIt = ObjectsCustomized.CreateConstIterator(); ObjectIt; ++ObjectIt) { if (ObjectIt->IsValid() && (*ObjectIt)->IsA(UPrimitiveComponent::StaticClass())) { TWeakObjectPtr SceneComponent = CastChecked(ObjectIt->Get()); if (SceneComponent.IsValid() && SceneComponent->Mobility != EComponentMobility::Movable) { bEnableUseAsyncScene = false; break; } // Skeletal mesh uses a physics asset which will have multiple bodies - these bodies have their own bUseAsyncScene which is what we actually use - the flag on the skeletal mesh is not used TWeakObjectPtr SkeletalMeshComponent = Cast(ObjectIt->Get()); if (SkeletalMeshComponent.IsValid()) { bEnableUseAsyncScene = false; break; } } else if (ObjectIt->IsValid() && (*ObjectIt)->IsA(UBodySetup::StaticClass())) { continue; } else { bEnableUseAsyncScene = false; break; } } return bEnableUseAsyncScene; } EVisibility FBodyInstanceCustomizationHelper::IsMassVisible(bool bOverrideMass) const { bool bIsMassReadOnly = IsBodyMassReadOnly(); if (bOverrideMass) { return bIsMassReadOnly ? EVisibility::Collapsed : EVisibility::Visible; } else { return bIsMassReadOnly ? EVisibility::Visible : EVisibility::Collapsed; } } bool FBodyInstanceCustomizationHelper::IsBodyMassReadOnly() const { for (auto ObjectIt = ObjectsCustomized.CreateConstIterator(); ObjectIt; ++ObjectIt) { if (ObjectIt->IsValid() && (*ObjectIt)->IsA(UPrimitiveComponent::StaticClass())) { if (UPrimitiveComponent* Comp = Cast(ObjectIt->Get())) { if (Comp->BodyInstance.bOverrideMass == false) { return true; } } }else if(ObjectIt->IsValid() && (*ObjectIt)->IsA(UBodySetup::StaticClass())) { UBodySetup* BS = Cast(ObjectIt->Get()); if (BS->DefaultInstance.bOverrideMass == false) { return true; } } } return false; } TOptional FBodyInstanceCustomizationHelper::OnGetBodyMaxAngularVelocity() const { UPrimitiveComponent* Comp = nullptr; const float MaxAngularVelocity = UPhysicsSettings::Get()->MaxAngularVelocity; for (auto ObjectIt = ObjectsCustomized.CreateConstIterator(); ObjectIt; ++ObjectIt) { if (ObjectIt->IsValid() && (*ObjectIt)->IsA(UPrimitiveComponent::StaticClass())) { Comp = Cast(ObjectIt->Get()); Comp->BodyInstance.MaxAngularVelocity = MaxAngularVelocity; //update max angular velocity so that overriding gives the same value initially } } return MaxAngularVelocity; } bool FBodyInstanceCustomizationHelper::IsMaxAngularVelocityReadOnly() const { for (auto ObjectIt = ObjectsCustomized.CreateConstIterator(); ObjectIt; ++ObjectIt) { if (ObjectIt->IsValid() && (*ObjectIt)->IsA(UPrimitiveComponent::StaticClass())) { if (UPrimitiveComponent* Comp = Cast(ObjectIt->Get())) { if (Comp->BodyInstance.bOverrideMaxAngularVelocity == false) { return true; } } } } return false; } EVisibility FBodyInstanceCustomizationHelper::IsMaxAngularVelocityVisible(bool bOverrideMaxAngularVelocity) const { bool bIsMaxAngularVelocityReadOnly = IsMaxAngularVelocityReadOnly(); if (bOverrideMaxAngularVelocity) { return bIsMaxAngularVelocityReadOnly ? EVisibility::Collapsed : EVisibility::Visible; } else { return bIsMaxAngularVelocityReadOnly ? EVisibility::Visible : EVisibility::Collapsed; } } bool FBodyInstanceCustomizationHelper::IsAutoWeldEditable() const { for (int32 i = 0; i < ObjectsCustomized.Num(); ++i) { if (UPrimitiveComponent * SceneComponent = Cast(ObjectsCustomized[i].Get())) { if (FBodyInstance* BI = SceneComponent->GetBodyInstance()) { if (BI->ShouldInstanceSimulatingPhysics()) { return false; } } } } return true; } EVisibility FBodyInstanceCustomizationHelper::IsAutoWeldVisible() const { for (int32 i = 0; i < ObjectsCustomized.Num(); ++i) { if (ObjectsCustomized[i].IsValid() && !(ObjectsCustomized[i]->IsA(UStaticMeshComponent::StaticClass()) || ObjectsCustomized[i]->IsA(UShapeComponent::StaticClass()))) { return EVisibility::Collapsed; } } return EVisibility::Visible; } TOptional FBodyInstanceCustomizationHelper::OnGetBodyMass() const { UPrimitiveComponent* Comp = nullptr; UBodySetup* BS = nullptr; float Mass = 0.0f; bool bMultipleValue = false; for (auto ObjectIt = ObjectsCustomized.CreateConstIterator(); ObjectIt; ++ObjectIt) { if (ObjectIt->IsValid() && (*ObjectIt)->IsA(UPrimitiveComponent::StaticClass())) { Comp = Cast(ObjectIt->Get()); float CompMass = Comp->CalculateMass(); Comp->BodyInstance.MassInKg = CompMass; //update the component's override mass to be the calculated one if (Mass == 0.0f || FMath::Abs(Mass - CompMass) < 0.1f) { Mass = CompMass; } else { bMultipleValue = true; break; } } else if (ObjectIt->IsValid() && (*ObjectIt)->IsA(UBodySetup::StaticClass())) { BS = Cast(ObjectIt->Get()); float BSMass = BS->CalculateMass(); if (Mass == 0.0f || FMath::Abs(Mass - BSMass) < 0.1f) { Mass = BSMass; } else { bMultipleValue = true; break; } } } if (bMultipleValue) { return TOptional(); } return Mass; } EVisibility FBodyInstanceCustomizationHelper::IsDOFMode(EDOFMode::Type Mode) const { bool bVisible = false; if (DOFModeProperty.IsValid() && bDisplayConstraints) { uint8 CurrentMode; if (DOFModeProperty->GetValue(CurrentMode) == FPropertyAccess::Success) { EDOFMode::Type PropertyDOF = FBodyInstance::ResolveDOFMode(static_cast(CurrentMode)); bVisible = PropertyDOF == Mode; } } return bVisible ? EVisibility::Visible : EVisibility::Collapsed; } void FBodyInstanceCustomizationHelper::AddMassInKg(IDetailCategoryBuilder& PhysicsCategory, TSharedRef BodyInstanceHandler) { if (bDisplayMass) { TSharedRef MassInKGHandle = BodyInstanceHandler->GetChildHandle(GET_MEMBER_NAME_CHECKED(FBodyInstance, MassInKg)).ToSharedRef(); PhysicsCategory.AddProperty(MassInKGHandle).CustomWidget() .NameContent() [ MassInKGHandle->CreatePropertyNameWidget() ] .ValueContent() [ SNew(SVerticalBox) + SVerticalBox::Slot() .Padding(0.f, 0.f, 10.f, 0.f) [ SNew(SNumericEntryBox) .IsEnabled(false) .Font(IDetailLayoutBuilder::GetDetailFont()) .Value(this, &FBodyInstanceCustomizationHelper::OnGetBodyMass) .Visibility(this, &FBodyInstanceCustomizationHelper::IsMassVisible, false) ] + SVerticalBox::Slot() .Padding(0.f, 0.f, 10.f, 0.f) [ SNew(SVerticalBox) .Visibility(this, &FBodyInstanceCustomizationHelper::IsMassVisible, true) + SVerticalBox::Slot() .AutoHeight() [ MassInKGHandle->CreatePropertyValueWidget() ] ] ]; } } void FBodyInstanceCustomizationHelper::AddMaxAngularVelocity(IDetailCategoryBuilder& PhysicsCategory, TSharedRef BodyInstanceHandler) { TSharedRef MaxAngularVelocityHandle = BodyInstanceHandler->GetChildHandle(GET_MEMBER_NAME_CHECKED(FBodyInstance, MaxAngularVelocity)).ToSharedRef(); PhysicsCategory.AddProperty(MaxAngularVelocityHandle).CustomWidget() .NameContent() [ MaxAngularVelocityHandle->CreatePropertyNameWidget() ] .ValueContent() [ SNew(SVerticalBox) + SVerticalBox::Slot() .Padding(0.f, 0.f, 10.f, 0.f) [ SNew(SNumericEntryBox) .IsEnabled(false) .Font(IDetailLayoutBuilder::GetDetailFont()) .Value(this, &FBodyInstanceCustomizationHelper::OnGetBodyMaxAngularVelocity) .Visibility(this, &FBodyInstanceCustomizationHelper::IsMaxAngularVelocityVisible, false) ] + SVerticalBox::Slot() .Padding(0.f, 0.f, 10.f, 0.f) [ SNew(SVerticalBox) .Visibility(this, &FBodyInstanceCustomizationHelper::IsMaxAngularVelocityVisible, true) + SVerticalBox::Slot() .AutoHeight() [ MaxAngularVelocityHandle->CreatePropertyValueWidget() ] ] ]; } void FBodyInstanceCustomizationHelper::AddBodyConstraint(IDetailCategoryBuilder& PhysicsCategory, TSharedRef BodyInstanceHandler) { const float XYZPadding = 5.f; TSharedPtr bLockXTranslation = BodyInstanceHandler->GetChildHandle(GET_MEMBER_NAME_CHECKED(FBodyInstance, bLockXTranslation)); bLockXTranslation->MarkHiddenByCustomization(); TSharedPtr bLockYTranslation = BodyInstanceHandler->GetChildHandle(GET_MEMBER_NAME_CHECKED(FBodyInstance, bLockYTranslation)); bLockYTranslation->MarkHiddenByCustomization(); TSharedPtr bLockZTranslation = BodyInstanceHandler->GetChildHandle(GET_MEMBER_NAME_CHECKED(FBodyInstance, bLockZTranslation)); bLockZTranslation->MarkHiddenByCustomization(); TSharedPtr bLockXRotation = BodyInstanceHandler->GetChildHandle(GET_MEMBER_NAME_CHECKED(FBodyInstance, bLockXRotation)); bLockXRotation->MarkHiddenByCustomization(); TSharedPtr bLockYRotation = BodyInstanceHandler->GetChildHandle(GET_MEMBER_NAME_CHECKED(FBodyInstance, bLockYRotation)); bLockYRotation->MarkHiddenByCustomization(); TSharedPtr bLockZRotation = BodyInstanceHandler->GetChildHandle(GET_MEMBER_NAME_CHECKED(FBodyInstance, bLockZRotation)); bLockZRotation->MarkHiddenByCustomization(); DOFModeProperty = BodyInstanceHandler->GetChildHandle(GET_MEMBER_NAME_CHECKED(FBodyInstance, DOFMode)).ToSharedRef(); DOFModeProperty->MarkHiddenByCustomization(); TSharedRef bLockRotation = BodyInstanceHandler->GetChildHandle(GET_MEMBER_NAME_CHECKED(FBodyInstance, bLockRotation)).ToSharedRef(); bLockRotation->MarkHiddenByCustomization(); TSharedRef bLockTranslation = BodyInstanceHandler->GetChildHandle(GET_MEMBER_NAME_CHECKED(FBodyInstance, bLockTranslation)).ToSharedRef(); bLockTranslation->MarkHiddenByCustomization(); TSharedRef CustomDOFPlaneNormal = BodyInstanceHandler->GetChildHandle(GET_MEMBER_NAME_CHECKED(FBodyInstance, CustomDOFPlaneNormal)).ToSharedRef(); CustomDOFPlaneNormal->MarkHiddenByCustomization(); //the above are all marked hidden even if we don't display constraints because the user wants to hide it anyway if (bDisplayConstraints) { IDetailGroup& ConstraintsGroup = PhysicsCategory.AddGroup(TEXT("ConstraintsGroup"), LOCTEXT("Constraints", "Constraints")); ConstraintsGroup.AddWidgetRow() .Visibility(TAttribute::Create(TAttribute::FGetter::CreateSP(this, &FBodyInstanceCustomizationHelper::IsDOFMode, EDOFMode::SixDOF))) .NameContent() [ SNew(STextBlock) .Text(LOCTEXT("LockPositionLabel", "Lock Position")) .ToolTipText(LOCTEXT("LockPositionTooltip", "Locks movement along the specified axis")) .Font(IDetailLayoutBuilder::GetDetailFont()) ] .ValueContent() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .Padding(0.f, 0.f, XYZPadding, 0.f) .AutoWidth() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() [ bLockXTranslation->CreatePropertyNameWidget() ] + SHorizontalBox::Slot() .AutoWidth() [ bLockXTranslation->CreatePropertyValueWidget() ] ] + SHorizontalBox::Slot() .Padding(0.f, 0.f, XYZPadding, 0.f) .AutoWidth() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() [ bLockYTranslation->CreatePropertyNameWidget() ] + SHorizontalBox::Slot() .AutoWidth() [ bLockYTranslation->CreatePropertyValueWidget() ] ] + SHorizontalBox::Slot() .Padding(0.f, 0.f, XYZPadding, 0.f) .AutoWidth() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() [ bLockZTranslation->CreatePropertyNameWidget() ] + SHorizontalBox::Slot() .AutoWidth() [ bLockZTranslation->CreatePropertyValueWidget() ] ] ]; ConstraintsGroup.AddWidgetRow() .Visibility(TAttribute::Create(TAttribute::FGetter::CreateSP(this, &FBodyInstanceCustomizationHelper::IsDOFMode, EDOFMode::SixDOF))) .NameContent() [ SNew(STextBlock) .Text(LOCTEXT("LockRotationLabel", "Lock Rotation")) .ToolTipText(LOCTEXT("LockRotationTooltip", "Locks rotation about the specified axis")) .Font(IDetailLayoutBuilder::GetDetailFont()) ] .ValueContent() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .Padding(0.f, 0.f, XYZPadding, 0.f) .AutoWidth() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() [ bLockXRotation->CreatePropertyNameWidget() ] + SHorizontalBox::Slot() .AutoWidth() [ bLockXRotation->CreatePropertyValueWidget() ] ] + SHorizontalBox::Slot() .Padding(0.f, 0.f, XYZPadding, 0.f) .AutoWidth() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() [ bLockYRotation->CreatePropertyNameWidget() ] + SHorizontalBox::Slot() .AutoWidth() [ bLockYRotation->CreatePropertyValueWidget() ] ] + SHorizontalBox::Slot() .Padding(0.f, 0.f, XYZPadding, 0.f) .AutoWidth() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() [ bLockZRotation->CreatePropertyNameWidget() ] + SHorizontalBox::Slot() .AutoWidth() [ bLockZRotation->CreatePropertyValueWidget() ] ] ]; //we only show the custom plane normal if we've selected that mode ConstraintsGroup.AddPropertyRow(CustomDOFPlaneNormal).Visibility(TAttribute::Create(TAttribute::FGetter::CreateSP(this, &FBodyInstanceCustomizationHelper::IsDOFMode, EDOFMode::CustomPlane))); ConstraintsGroup.AddPropertyRow(bLockTranslation).Visibility(TAttribute::Create(TAttribute::FGetter::CreateSP(this, &FBodyInstanceCustomizationHelper::IsDOFMode, EDOFMode::CustomPlane))); ConstraintsGroup.AddPropertyRow(bLockRotation).Visibility(TAttribute::Create(TAttribute::FGetter::CreateSP(this, &FBodyInstanceCustomizationHelper::IsDOFMode, EDOFMode::CustomPlane))); ConstraintsGroup.AddPropertyRow(DOFModeProperty.ToSharedRef()); } } //////////////////////////////////////////////////////////////// #undef LOCTEXT_NAMESPACE #undef RowWidth_Customization