// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. #include "AnimGraphNode_SubInstance.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Input/SCheckBox.h" #include "EdGraphSchema_K2.h" #include "Kismet2/CompilerResultsLog.h" #include "Kismet2/BlueprintEditorUtils.h" #include "PropertyHandle.h" #include "DetailLayoutBuilder.h" #include "DetailCategoryBuilder.h" #include "Animation/AnimNode_SubInput.h" #include "PropertyCustomizationHelpers.h" #include "ScopedTransaction.h" #include "AnimationGraphSchema.h" #define LOCTEXT_NAMESPACE "SubInstanceNode" namespace SubInstanceGraphNodeConstants { FLinearColor TitleColor(0.2f, 0.2f, 0.8f); } FLinearColor UAnimGraphNode_SubInstance::GetNodeTitleColor() const { return SubInstanceGraphNodeConstants::TitleColor; } FText UAnimGraphNode_SubInstance::GetTooltipText() const { return LOCTEXT("ToolTip", "Runs a sub-anim instance to process animation"); } FText UAnimGraphNode_SubInstance::GetNodeTitle(ENodeTitleType::Type TitleType) const { UClass* TargetClass = *Node.InstanceClass; FFormatNamedArguments Args; Args.Add(TEXT("NodeTitle"), LOCTEXT("Title", "Sub Anim Instance")); Args.Add(TEXT("TargetClass"), TargetClass ? FText::FromString(TargetClass->GetName()) : LOCTEXT("ClassNone", "None")); if(TitleType == ENodeTitleType::MenuTitle) { return LOCTEXT("NodeTitle", "Sub Anim Instance"); } if(TitleType == ENodeTitleType::ListView) { if(Node.Tag != NAME_None) { Args.Add(TEXT("Tag"), FText::FromName(Node.Tag)); return FText::Format(LOCTEXT("TitleListFormatTagged", "{NodeTitle} - Target Class: {TargetClass} - Tag: {Tag}"), Args); } else { return FText::Format(LOCTEXT("TitleListFormat", "{NodeTitle} - Target Class: {TargetClass}"), Args); } } else { if(Node.Tag != NAME_None) { Args.Add(TEXT("Tag"), FText::FromName(Node.Tag)); return FText::Format(LOCTEXT("TitleFormatTagged", "{NodeTitle}\nTarget Class: {TargetClass}\nTag: {Tag}"), Args); } else { return FText::Format(LOCTEXT("TitleFormat", "{NodeTitle}\nTarget Class: {TargetClass}"), Args); } } } void UAnimGraphNode_SubInstance::ValidateAnimNodeDuringCompilation(USkeleton* ForSkeleton, FCompilerResultsLog& MessageLog) { Super::ValidateAnimNodeDuringCompilation(ForSkeleton, MessageLog); UAnimBlueprint* AnimBP = CastChecked(GetBlueprint()); UObject* OriginalNode = MessageLog.FindSourceObject(this); // Check we have a class set if(!*Node.InstanceClass) { MessageLog.Error(TEXT("Sub instance node @@ has no valid instance class to spawn."), this); } if(HasInstanceLoop()) { MessageLog.Error(TEXT("Detected loop in sub instance chain starting at @@ inside class @@"), this, AnimBP->GetAnimBlueprintGeneratedClass()); } // Check for cycles from other sub instance nodes TArray Graphs; AnimBP->GetAllGraphs(Graphs); // Check for duplicate tags in this anim blueprint for(UEdGraph* Graph : Graphs) { TArray SubInstanceNodes; Graph->GetNodesOfClass(SubInstanceNodes); for(UAnimGraphNode_SubInstance* SubInstanceNode : SubInstanceNodes) { if(SubInstanceNode == OriginalNode) { continue; } FAnimNode_SubInstance& InnerNode = SubInstanceNode->Node; if(InnerNode.Tag != NAME_None && InnerNode.Tag == Node.Tag) { MessageLog.Error(*FText::Format(LOCTEXT("DuplicateTagErrorFormat", "Node @@ and node @@ both have the same tag '{0}'."), FText::FromName(Node.Tag)).ToString(), this, SubInstanceNode); } } } // Check we don't try to spawn our own blueprint if(*Node.InstanceClass == AnimBP->GetAnimBlueprintGeneratedClass()) { MessageLog.Error(TEXT("Sub instance node @@ targets instance class @@ which it is inside, this would cause a loop."), this, AnimBP->GetAnimBlueprintGeneratedClass()); } } void UAnimGraphNode_SubInstance::ReallocatePinsDuringReconstruction(TArray& OldPins) { Super::ReallocatePinsDuringReconstruction(OldPins); UClass* TargetClass = *Node.InstanceClass; if(!TargetClass) { // Nothing to search for properties return; } // Need the schema to extract pin types const UEdGraphSchema_K2* Schema = CastChecked(GetSchema()); // Default anim schema for util funcions const UAnimationGraphSchema* AnimGraphDefaultSchema = GetDefault(); bool bShowPose = false; // Scan the target class for a sub input node, we only want to show the pose input if // we have that node available for(TFieldIterator It(TargetClass); It; ++It) { UProperty* CurrentProp = *It; if(UStructProperty* StructProp = Cast(CurrentProp)) { if(StructProp->Struct->IsChildOf(FAnimNode_SubInput::StaticStruct())) { // Found a pose input bShowPose = true; break; } } } if(bShowPose) { if(UProperty* PoseProperty = FindField(FAnimNode_SubInstance::StaticStruct(), GET_MEMBER_NAME_CHECKED(FAnimNode_SubInstance, InPose))) { FEdGraphPinType PinType; if(Schema->ConvertPropertyToPinType(PoseProperty, PinType)) { UEdGraphPin* NewPin = CreatePin(EEdGraphPinDirection::EGPD_Input, PinType, PoseProperty->GetFName()); NewPin->PinFriendlyName = PoseProperty->GetDisplayNameText(); CustomizePinData(NewPin, PoseProperty->GetFName(), INDEX_NONE); } } } // Grab the list of properties we can expose TArray ExposablePropeties; GetExposableProperties(ExposablePropeties); // We'll track the names we encounter by removing from this list, if anything remains the properties // have been removed from the target class and we should remove them too TArray BeginExposableNames = KnownExposableProperties; for(UProperty* Property : ExposablePropeties) { FName PropertyName = Property->GetFName(); BeginExposableNames.Remove(PropertyName); if(!KnownExposableProperties.Contains(PropertyName)) { // New property added to the target class KnownExposableProperties.Add(PropertyName); } if(ExposedPropertyNames.Contains(PropertyName) && FBlueprintEditorUtils::PropertyStillExists(Property)) { FEdGraphPinType PinType; verify(Schema->ConvertPropertyToPinType(Property, PinType)); UEdGraphPin* NewPin = CreatePin(EEdGraphPinDirection::EGPD_Input, PinType, Property->GetFName()); NewPin->PinFriendlyName = Property->GetDisplayNameText(); // Need to grab the default value for the property from the target generated class CDO FString CDODefaultValueString; uint8* ContainerPtr = reinterpret_cast(TargetClass->GetDefaultObject()); if(FBlueprintEditorUtils::PropertyValueToString(Property, ContainerPtr, CDODefaultValueString)) { // If we successfully pulled a value, set it to the pin Schema->TrySetDefaultValue(*NewPin, CDODefaultValueString); } CustomizePinData(NewPin, PropertyName, INDEX_NONE); } } // Remove any properties that no longer exist on the target class for(FName& RemovedPropertyName : BeginExposableNames) { KnownExposableProperties.Remove(RemovedPropertyName); } } void UAnimGraphNode_SubInstance::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) { Super::PostEditChangeProperty(PropertyChangedEvent); bool bRequiresNodeReconstruct = false; UProperty* ChangedProperty = PropertyChangedEvent.Property; if(ChangedProperty) { if(ChangedProperty->GetFName() == GET_MEMBER_NAME_CHECKED(FAnimNode_SubInstance, InstanceClass)) { bRequiresNodeReconstruct = true; RebuildExposedProperties(*Node.InstanceClass); } } if(bRequiresNodeReconstruct) { ReconstructNode(); } } void UAnimGraphNode_SubInstance::GetInstancePinProperty(const UClass* InOwnerInstanceClass, UEdGraphPin* InInputPin, UProperty*& OutProperty) { // The actual name of the instance property FString FullName = GetPinTargetVariableName(InInputPin); if(UProperty* Property = FindField(InOwnerInstanceClass, *FullName)) { OutProperty = Property; } else { OutProperty = nullptr; } } FString UAnimGraphNode_SubInstance::GetPinTargetVariableName(const UEdGraphPin* InPin) const { return TEXT("__SUBINSTANCE_") + InPin->PinName.ToString() + TEXT("_") + NodeGuid.ToString(); } void UAnimGraphNode_SubInstance::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) { Super::CustomizeDetails(DetailBuilder); // We dont allow multi-select here if(DetailBuilder.GetSelectedObjects().Num() > 1) { return; } TArray ExposableProperties; GetExposableProperties(ExposableProperties); if(ExposableProperties.Num() > 0) { IDetailCategoryBuilder& CategoryBuilder = DetailBuilder.EditCategory(FName(TEXT("Sub Instance Properties"))); for(UProperty* Property : ExposableProperties) { FDetailWidgetRow& WidgetRow = CategoryBuilder.AddCustomRow(FText::FromString(Property->GetName())); FName PropertyName = Property->GetFName(); FText PropertyTypeText = GetPropertyTypeText(Property); FFormatNamedArguments Args; Args.Add(TEXT("PropertyName"), FText::FromName(PropertyName)); Args.Add(TEXT("PropertyType"), PropertyTypeText); FText TooltipText = FText::Format(LOCTEXT("PropertyTooltipText", "{PropertyName}\nType: {PropertyType}"), Args); WidgetRow.NameContent() [ SNew(STextBlock) .Text(FText::FromString(Property->GetName())) .ToolTipText(TooltipText) ]; WidgetRow.ValueContent() [ SNew(SHorizontalBox) +SHorizontalBox::Slot() [ SNew(STextBlock) .Text(LOCTEXT("ExposePropertyValue", "Expose: ")) ] +SHorizontalBox::Slot() [ SNew(SCheckBox) .IsChecked_UObject(this, &UAnimGraphNode_SubInstance::IsPropertyExposed, PropertyName) .OnCheckStateChanged_UObject(this, &UAnimGraphNode_SubInstance::OnPropertyExposeCheckboxChanged, PropertyName) ] ]; } } TSharedRef ClassHandle = DetailBuilder.GetProperty(TEXT("Node.InstanceClass"), GetClass()); if(ClassHandle->IsValidHandle()) { ClassHandle->SetOnPropertyValueChanged(FSimpleDelegate::CreateUObject(this, &UAnimGraphNode_SubInstance::OnInstanceClassChanged, &DetailBuilder)); } ClassHandle->MarkHiddenByCustomization(); IDetailCategoryBuilder& CategoryBuilder = DetailBuilder.EditCategory(FName(TEXT("Settings"))); FDetailWidgetRow& ClassWidgetRow = CategoryBuilder.AddCustomRow(LOCTEXT("FilterString", "Instance Class")); ClassWidgetRow.NameContent() [ ClassHandle->CreatePropertyNameWidget() ]; ClassWidgetRow.ValueContent() [ SNew(SObjectPropertyEntryBox) .ObjectPath_UObject(this, &UAnimGraphNode_SubInstance::GetCurrentInstanceBlueprintPath) .AllowedClass(UAnimBlueprint::StaticClass()) .NewAssetFactories(TArray()) .OnShouldFilterAsset(FOnShouldFilterAsset::CreateUObject(this, &UAnimGraphNode_SubInstance::OnShouldFilterInstanceBlueprint)) .OnObjectChanged(FOnSetObject::CreateUObject(this, &UAnimGraphNode_SubInstance::OnSetInstanceBlueprint, ClassHandle)) ]; } FText UAnimGraphNode_SubInstance::GetPropertyTypeText(UProperty* Property) { FText PropertyTypeText; if(UStructProperty* StructProperty = Cast(Property)) { PropertyTypeText = StructProperty->Struct->GetDisplayNameText(); } else if(UObjectProperty* ObjectProperty = Cast(Property)) { PropertyTypeText = ObjectProperty->PropertyClass->GetDisplayNameText(); } else if(UClass* PropClass = Property->GetClass()) { PropertyTypeText = PropClass->GetDisplayNameText(); } else { PropertyTypeText = LOCTEXT("PropertyTypeUnknown", "Unknown"); } return PropertyTypeText; } void UAnimGraphNode_SubInstance::RebuildExposedProperties(UClass* InNewClass) { ExposedPropertyNames.Empty(); KnownExposableProperties.Empty(); if(InNewClass) { TArray ExposableProperties; GetExposableProperties(ExposableProperties); for(UProperty* Property : ExposableProperties) { KnownExposableProperties.Add(Property->GetFName()); } } } bool UAnimGraphNode_SubInstance::HasInstanceLoop() { TArray VisitedList; TArray CurrentStack; return HasInstanceLoop_Recursive(this, VisitedList, CurrentStack); } bool UAnimGraphNode_SubInstance::HasInstanceLoop_Recursive(UAnimGraphNode_SubInstance* CurrNode, TArray& VisitedNodes, TArray& NodeStack) { if(!VisitedNodes.Contains(CurrNode->NodeGuid)) { VisitedNodes.Add(CurrNode->NodeGuid); NodeStack.Add(CurrNode->NodeGuid); if(UAnimBlueprint* AnimBP = Cast(UBlueprint::GetBlueprintFromClass(CurrNode->Node.InstanceClass))) { // Check for cycles from other sub instance nodes TArray Graphs; AnimBP->GetAllGraphs(Graphs); for(UEdGraph* Graph : Graphs) { TArray SubInstanceNodes; Graph->GetNodesOfClass(SubInstanceNodes); for(UAnimGraphNode_SubInstance* SubInstanceNode : SubInstanceNodes) { // If we haven't visited this node, then check it for loops, otherwise if we're pointing to a previously visited node that is in the current instance stack we have a loop if((!VisitedNodes.Contains(SubInstanceNode->NodeGuid) && HasInstanceLoop_Recursive(SubInstanceNode, VisitedNodes, NodeStack)) || NodeStack.Contains(SubInstanceNode->NodeGuid)) { return true; } } } } } NodeStack.Remove(CurrNode->NodeGuid); return false; } ECheckBoxState UAnimGraphNode_SubInstance::IsPropertyExposed(FName PropertyName) const { return ExposedPropertyNames.Contains(PropertyName) ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } void UAnimGraphNode_SubInstance::OnPropertyExposeCheckboxChanged(ECheckBoxState NewState, FName PropertyName) { if(NewState == ECheckBoxState::Checked) { ExposedPropertyNames.AddUnique(PropertyName); } else if(NewState == ECheckBoxState::Unchecked) { ExposedPropertyNames.Remove(PropertyName); } ReconstructNode(); } void UAnimGraphNode_SubInstance::OnInstanceClassChanged(IDetailLayoutBuilder* DetailBuilder) { if(DetailBuilder) { DetailBuilder->ForceRefreshDetails(); } } FString UAnimGraphNode_SubInstance::GetCurrentInstanceBlueprintPath() const { UClass* InstanceClass = *Node.InstanceClass; if(InstanceClass) { UBlueprint* ActualBlueprint = UBlueprint::GetBlueprintFromClass(InstanceClass); if(ActualBlueprint) { return ActualBlueprint->GetPathName(); } } return FString(); } bool UAnimGraphNode_SubInstance::OnShouldFilterInstanceBlueprint(const FAssetData& AssetData) const { FAssetDataTagMapSharedView::FFindTagResult Result = AssetData.TagsAndValues.FindTag("TargetSkeleton"); if (Result.IsSet()) { if (UAnimBlueprint* CurrentBlueprint = Cast(GetBlueprint())) { FString TargetSkeletonName = FString::Printf(TEXT("%s'%s'"), *CurrentBlueprint->TargetSkeleton->GetClass()->GetName(), *CurrentBlueprint->TargetSkeleton->GetPathName()); return Result.GetValue() != TargetSkeletonName; } } return false; } void UAnimGraphNode_SubInstance::OnSetInstanceBlueprint(const FAssetData& AssetData, TSharedRef InstanceClassPropHandle) { if(UAnimBlueprint* Blueprint = Cast(AssetData.GetAsset())) { FScopedTransaction Transaction(LOCTEXT("SetBP", "Set Instance Blueprint")); Modify(); InstanceClassPropHandle->SetValue(Blueprint->GetAnimBlueprintGeneratedClass()); } } UObject* UAnimGraphNode_SubInstance::GetJumpTargetForDoubleClick() const { UClass* InstanceClass = *Node.InstanceClass; if(InstanceClass) { return InstanceClass->ClassGeneratedBy; } return nullptr; } bool UAnimGraphNode_SubInstance::HasExternalDependencies(TArray* OptionalOutput /*= NULL*/) const { UClass* InstanceClassToUse = *Node.InstanceClass; // Add our instance class... If that changes we need a recompile if(InstanceClassToUse && OptionalOutput) { OptionalOutput->AddUnique(InstanceClassToUse); } bool bSuperResult = Super::HasExternalDependencies(OptionalOutput); return InstanceClassToUse || bSuperResult; } void UAnimGraphNode_SubInstance::GetExposableProperties(TArray& OutExposableProperties) const { OutExposableProperties.Empty(); UClass* TargetClass = *Node.InstanceClass; if(TargetClass) { const UEdGraphSchema_K2* Schema = CastChecked(GetSchema()); for(TFieldIterator It(TargetClass, EFieldIteratorFlags::IncludeSuper); It; ++It) { UProperty* CurProperty = *It; FEdGraphPinType PinType; if(CurProperty->HasAllPropertyFlags(CPF_Edit | CPF_BlueprintVisible) && CurProperty->HasAllFlags(RF_Public) && Schema->ConvertPropertyToPinType(CurProperty, PinType)) { OutExposableProperties.Add(CurProperty); } } } } #undef LOCTEXT_NAMESPACE