Files
UnrealEngineUWP/Engine/Source/Editor/AnimGraph/Private/AnimGraphNode_LinkedInputPose.cpp
Thomas Sarkanen b85a3cea8c Animation sync node and anim graph attributes
Refactored tick record sync into utility structure - FAnimSync. This improves the odd API around tick records, better encapsulating functionality and leaving less for the caller to get wrong. This will also eventually allow this to be refactored out into a scriptable pipeline stage.
Added sync node and scoped sync message for new 'graph based sync'. Nodes that subscribe to graph-based-sync determine their sync group based on the scope that they are in.
Removed 4.26-style sync scopes - pushed all syncing up to the main anim instance. Linked anim instances no longer sync their own tick records.
To make graph based sync more useful, surfaced graph attributes and their visualizations as labels on pins and parallel wires to visualize flow.
This involves statically determining the attribute flow of the graph at compile time. Added a new compiler handler to deal with this new debug data.
Updated a lot of nodes to specify their attributes so graph flow can be correctly visualized.
Added tracing of attributes and sync records and visualization of traced records when debugging the anim graph.

#rb Jurre.deBaare, Martin.Wilson

[CL 14998555 by Thomas Sarkanen in ue5-main branch]
2021-01-06 09:11:59 -04:00

647 lines
20 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "AnimGraphNode_LinkedInputPose.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "GraphEditorSettings.h"
#include "BlueprintActionFilter.h"
#include "Widgets/Input/SButton.h"
#include "DetailCategoryBuilder.h"
#include "IDetailPropertyRow.h"
#include "DetailWidgetRow.h"
#include "EditorStyleSet.h"
#include "DetailLayoutBuilder.h"
#include "Widgets/Images/SImage.h"
#include "Widgets/Text/STextBlock.h"
#include "Stats/Stats.h"
#include "Animation/AnimBlueprint.h"
#include "IAnimationBlueprintEditor.h"
#include "AnimationGraphSchema.h"
#include "Widgets/Input/SEditableTextBox.h"
#include "Widgets/Layout/SBox.h"
#include "Widgets/SBoxPanel.h"
#include "K2Node_CallFunction.h"
#include "Containers/Ticker.h"
#include "Widgets/Input/SCheckBox.h"
#include "Subsystems/AssetEditorSubsystem.h"
#include "KismetCompiler.h"
#include "K2Node_VariableGet.h"
#include "AnimBlueprintCompiler.h"
#include "AnimGraphAttributes.h"
#define LOCTEXT_NAMESPACE "LinkedInputPose"
UAnimGraphNode_LinkedInputPose::UAnimGraphNode_LinkedInputPose()
: InputPoseIndex(INDEX_NONE)
{
}
void UAnimGraphNode_LinkedInputPose::CreateClassVariablesFromBlueprint(IAnimBlueprintVariableCreationContext& InCreationContext)
{
IterateFunctionParameters([this, &InCreationContext](const FName& InName, FEdGraphPinType InPinType)
{
if(!UAnimationGraphSchema::IsPosePin(InPinType))
{
UEdGraphPin* Pin = FindPin(InName, EGPD_Output);
if(Pin && Pin->LinkedTo.Num() > 0)
{
// create properties for 'local' linked input pose pins
FProperty* NewLinkedInputPoseProperty = InCreationContext.CreateVariable(InName, InPinType);
if(NewLinkedInputPoseProperty)
{
NewLinkedInputPoseProperty->SetPropertyFlags(CPF_BlueprintReadOnly);
}
}
}
});
}
void UAnimGraphNode_LinkedInputPose::ExpandNode(class FKismetCompilerContext& InCompilerContext, UEdGraph* InSourceGraph)
{
IterateFunctionParameters([this, &InCompilerContext](const FName& InName, FEdGraphPinType InPinType)
{
if(!UAnimationGraphSchema::IsPosePin(InPinType))
{
if(InCompilerContext.bIsFullCompile)
{
// Find the property we created in CreateClassVariablesFromBlueprint()
FProperty* LinkedInputPoseProperty = FindFProperty<FProperty>(InCompilerContext.NewClass, InName);
if(LinkedInputPoseProperty)
{
UEdGraphPin* Pin = FindPin(InName, EGPD_Output);
if(Pin && Pin->LinkedTo.Num() > 0)
{
// Create new node for property access
UK2Node_VariableGet* VariableGetNode = InCompilerContext.SpawnIntermediateNode<UK2Node_VariableGet>(this, GetGraph());
VariableGetNode->SetFromProperty(LinkedInputPoseProperty, true, LinkedInputPoseProperty->GetOwnerClass());
VariableGetNode->AllocateDefaultPins();
// Add pin to generated variable association, used for pin watching
UEdGraphPin* TrueSourcePin = InCompilerContext.MessageLog.FindSourcePin(Pin);
if (TrueSourcePin)
{
InCompilerContext.NewClass->GetDebugData().RegisterClassPropertyAssociation(TrueSourcePin, LinkedInputPoseProperty);
}
// link up to new node - note that this is not a FindPinChecked because if an interface changes without the
// implementing class being loaded, then its graphs will not be conformed until AFTER the skeleton class
// has been compiled, so the variable cannot be created. This also doesnt matter, as there wont be anything connected
// to the pin yet anyways.
UEdGraphPin* VariablePin = VariableGetNode->FindPin(LinkedInputPoseProperty->GetFName());
if(VariablePin)
{
TArray<UEdGraphPin*> Links = Pin->LinkedTo;
Pin->BreakAllPinLinks();
for(UEdGraphPin* LinkPin : Links)
{
VariablePin->MakeLinkTo(LinkPin);
}
}
}
}
}
}
});
}
void UAnimGraphNode_LinkedInputPose::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
Super::PostEditChangeProperty(PropertyChangedEvent);
if(PropertyChangedEvent.Property)
{
if( PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED(UAnimGraphNode_LinkedInputPose, Inputs) ||
PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED(UAnimGraphNode_LinkedInputPose, Node.Name) ||
PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED(FAnimBlueprintFunctionPinInfo, Name) ||
PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED(FAnimBlueprintFunctionPinInfo, Type))
{
HandleInputPinArrayChanged();
ReconstructNode();
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(GetAnimBlueprint());
ReconstructNode();
}
}
}
FLinearColor UAnimGraphNode_LinkedInputPose::GetNodeTitleColor() const
{
return GetDefault<UGraphEditorSettings>()->ResultNodeTitleColor;
}
FText UAnimGraphNode_LinkedInputPose::GetTooltipText() const
{
return LOCTEXT("ToolTip", "Inputs to a sub-animation graph from a parent instance.");
}
FText UAnimGraphNode_LinkedInputPose::GetNodeTitle(ENodeTitleType::Type TitleType) const
{
FText DefaultTitle = LOCTEXT("Title", "Input Pose");
if(TitleType != ENodeTitleType::FullTitle)
{
return DefaultTitle;
}
else
{
if(Node.Name != NAME_None)
{
FFormatNamedArguments Args;
Args.Add(TEXT("NodeTitle"), DefaultTitle);
Args.Add(TEXT("Name"), FText::FromName(Node.Name));
return FText::Format(LOCTEXT("TitleListFormatTagged", "{NodeTitle}\n{Name}"), Args);
}
else
{
return DefaultTitle;
}
}
}
bool UAnimGraphNode_LinkedInputPose::CanUserDeleteNode() const
{
// Only allow linked input poses to be deleted if their parent graph is mutable
// Also allow anim graphs to delete these nodes even theough they are 'read-only'
return GetGraph()->bAllowDeletion || GetGraph()->GetFName() == UEdGraphSchema_K2::GN_AnimGraph;
}
bool UAnimGraphNode_LinkedInputPose::CanDuplicateNode() const
{
return false;
}
template <typename Predicate>
static FName CreateUniqueName(const FName& InBaseName, Predicate IsUnique)
{
FName CurrentName = InBaseName;
int32 CurrentIndex = 0;
while (!IsUnique(CurrentName))
{
FString PossibleName = InBaseName.ToString() + TEXT("_") + FString::FromInt(CurrentIndex++);
CurrentName = FName(*PossibleName);
}
return CurrentName;
}
void UAnimGraphNode_LinkedInputPose::HandleInputPinArrayChanged()
{
TArray<UAnimGraphNode_LinkedInputPose*> LinkedInputPoseNodes;
UAnimBlueprint* AnimBlueprint = GetAnimBlueprint();
for(UEdGraph* Graph : AnimBlueprint->FunctionGraphs)
{
if(Graph->Schema->IsChildOf(UAnimationGraphSchema::StaticClass()))
{
// Create a unique name for this new linked input pose
Graph->GetNodesOfClass(LinkedInputPoseNodes);
}
}
for(FAnimBlueprintFunctionPinInfo& Input : Inputs)
{
// New names are created empty, so assign a unique name
if(Input.Name == NAME_None)
{
Input.Name = CreateUniqueName(TEXT("InputParam"), [&LinkedInputPoseNodes](FName InName)
{
for(UAnimGraphNode_LinkedInputPose* LinkedInputPoseNode : LinkedInputPoseNodes)
{
for(const FAnimBlueprintFunctionPinInfo& Input : LinkedInputPoseNode->Inputs)
{
if(Input.Name == InName)
{
return false;
}
}
}
return true;
});
if(Input.Type.PinCategory == NAME_None)
{
IAssetEditorInstance* AssetEditor = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()->FindEditorForAsset(AnimBlueprint, false);
check(AssetEditor->GetEditorName() == "AnimationBlueprintEditor");
IAnimationBlueprintEditor* AnimationBlueprintEditor = static_cast<IAnimationBlueprintEditor*>(AssetEditor);
Input.Type = AnimationBlueprintEditor->GetLastGraphPinTypeUsed();
}
}
}
bool bIsInterface = AnimBlueprint->BlueprintType == BPTYPE_Interface;
if(bIsInterface)
{
UAnimationGraphSchema::AutoArrangeInterfaceGraph(*GetGraph());
}
}
void UAnimGraphNode_LinkedInputPose::AllocatePinsInternal()
{
// use member reference if valid
if (UFunction* Function = FunctionReference.ResolveMember<UFunction>(GetBlueprintClassFromNode()))
{
CreatePinsFromStubFunction(Function);
}
if(IsEditable())
{
// use user-defined pins
CreateUserDefinedPins();
}
}
void UAnimGraphNode_LinkedInputPose::AllocateDefaultPins()
{
Super::AllocateDefaultPins();
AllocatePinsInternal();
}
void UAnimGraphNode_LinkedInputPose::ReallocatePinsDuringReconstruction(TArray<UEdGraphPin*>& OldPins)
{
Super::ReallocatePinsDuringReconstruction(OldPins);
AllocatePinsInternal();
}
void UAnimGraphNode_LinkedInputPose::CreateUserDefinedPins()
{
for(FAnimBlueprintFunctionPinInfo& PinInfo : Inputs)
{
UEdGraphPin* NewPin = CreatePin(EEdGraphPinDirection::EGPD_Output, PinInfo.Type, PinInfo.Name);
NewPin->PinFriendlyName = FText::FromName(PinInfo.Name);
}
}
void UAnimGraphNode_LinkedInputPose::CreatePinsFromStubFunction(const UFunction* Function)
{
const UEdGraphSchema_K2* K2Schema = GetDefault<UEdGraphSchema_K2>();
IterateFunctionParameters([this, K2Schema, Function](const FName& InName, const FEdGraphPinType& InPinType)
{
if(!UAnimationGraphSchema::IsPosePin(InPinType))
{
UEdGraphPin* Pin = CreatePin(EGPD_Output, InPinType, InName);
K2Schema->SetPinAutogeneratedDefaultValueBasedOnType(Pin);
UK2Node_CallFunction::GeneratePinTooltipFromFunction(*Pin, Function);
}
});
}
void UAnimGraphNode_LinkedInputPose::ConformInputPoseName()
{
IterateFunctionParameters([this](const FName& InName, const FEdGraphPinType& InPinType)
{
if(UAnimationGraphSchema::IsPosePin(InPinType))
{
Node.Name = InName;
}
});
}
bool UAnimGraphNode_LinkedInputPose::ValidateAgainstFunctionReference() const
{
bool bValid = false;
IterateFunctionParameters([this, &bValid](const FName& InName, const FEdGraphPinType& InPinType)
{
bValid = true;
});
return bValid;
}
void UAnimGraphNode_LinkedInputPose::PostPlacedNewNode()
{
if(IsEditable())
{
TArray<UAnimGraphNode_LinkedInputPose*> LinkedInputPoseNodes;
UAnimBlueprint* AnimBlueprint = CastChecked<UAnimBlueprint>(GetGraph()->GetOuter());
for(UEdGraph* Graph : AnimBlueprint->FunctionGraphs)
{
if(Graph->Schema->IsChildOf(UAnimationGraphSchema::StaticClass()))
{
// Create a unique name for this new linked input pose
Graph->GetNodesOfClass(LinkedInputPoseNodes);
}
}
Node.Name = CreateUniqueName(FAnimNode_LinkedInputPose::DefaultInputPoseName, [this, &LinkedInputPoseNodes](const FName& InNameToCheck)
{
for(UAnimGraphNode_LinkedInputPose* LinkedInputPoseNode : LinkedInputPoseNodes)
{
if(LinkedInputPoseNode != this && LinkedInputPoseNode->Node.Name == InNameToCheck)
{
return false;
}
}
return true;
});
FTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateLambda([WeakThis = TWeakObjectPtr<UAnimGraphNode_LinkedInputPose>(this)](float InDeltaTime)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_UAnimGraphNode_LinkedInputPose_PostPlacedNewNode);
if(UAnimGraphNode_LinkedInputPose* LinkedInputPoseNode = WeakThis.Get())
{
// refresh the BP editor's details panel in case we are viewing the graph
IAssetEditorInstance* AssetEditor = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()->FindEditorForAsset(LinkedInputPoseNode->GetAnimBlueprint(), false);
check(AssetEditor->GetEditorName() == "AnimationBlueprintEditor");
IAnimationBlueprintEditor* AnimationBlueprintEditor = static_cast<IAnimationBlueprintEditor*>(AssetEditor);
AnimationBlueprintEditor->RefreshInspector();
}
return false;
}));
}
}
class SLinkedInputPoseNodeLabelWidget : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SLinkedInputPoseNodeLabelWidget) {}
SLATE_END_ARGS()
void Construct(const FArguments& InArgs, TSharedPtr<IPropertyHandle> InNamePropertyHandle, UAnimGraphNode_LinkedInputPose* InLinkedInputPoseNode)
{
NamePropertyHandle = InNamePropertyHandle;
WeakLinkedInputPoseNode = InLinkedInputPoseNode;
ChildSlot
[
SAssignNew(NameTextBox, SEditableTextBox)
.Font(IDetailLayoutBuilder::GetDetailFont())
.Text(this, &SLinkedInputPoseNodeLabelWidget::HandleGetNameText)
.OnTextChanged(this, &SLinkedInputPoseNodeLabelWidget::HandleTextChanged)
.OnTextCommitted(this, &SLinkedInputPoseNodeLabelWidget::HandleTextCommitted)
];
}
FText HandleGetNameText() const
{
return FText::FromName(WeakLinkedInputPoseNode->Node.Name);
}
bool IsNameValid(const FString& InNewName, FText& OutReason)
{
if(InNewName.Len() == 0)
{
OutReason = LOCTEXT("ZeroSizeLinkedInputPoseError", "A name must be specified.");
return false;
}
else if(InNewName.Equals(TEXT("None"), ESearchCase::IgnoreCase))
{
OutReason = LOCTEXT("LinkedInputPoseInvalidName", "This name is invalid.");
return false;
}
else
{
UAnimBlueprint* AnimBlueprint = CastChecked<UAnimBlueprint>(WeakLinkedInputPoseNode->GetGraph()->GetOuter());
for(UEdGraph* Graph : AnimBlueprint->FunctionGraphs)
{
if(Graph->Schema->IsChildOf(UAnimationGraphSchema::StaticClass()))
{
TArray<UAnimGraphNode_LinkedInputPose*> LinkedInputPoseNodes;
Graph->GetNodesOfClass(LinkedInputPoseNodes);
for(UAnimGraphNode_LinkedInputPose* LinkedInputPoseNode : LinkedInputPoseNodes)
{
if(LinkedInputPoseNode != WeakLinkedInputPoseNode.Get() && LinkedInputPoseNode->Node.Name.ToString().Equals(InNewName, ESearchCase::IgnoreCase))
{
OutReason = LOCTEXT("DuplicateLinkedInputPoseError", "This linked input pose name already exists in this blueprint.");
return false;
}
}
}
}
return true;
}
}
void HandleTextChanged(const FText& InNewText)
{
const FString NewTextAsString = InNewText.ToString();
FText Reason;
if(!IsNameValid(NewTextAsString, Reason))
{
NameTextBox->SetError(Reason);
}
else
{
NameTextBox->SetError(FText::GetEmpty());
}
}
void HandleTextCommitted(const FText& InNewText, ETextCommit::Type InCommitType)
{
const FString NewTextAsString = InNewText.ToString();
FText Reason;
if(IsNameValid(NewTextAsString, Reason))
{
FName NewName = *InNewText.ToString();
NamePropertyHandle->SetValue(NewName);
}
NameTextBox->SetError(FText::GetEmpty());
}
TSharedPtr<SEditableTextBox> NameTextBox;
TSharedPtr<IPropertyHandle> NamePropertyHandle;
TWeakObjectPtr<UAnimGraphNode_LinkedInputPose> WeakLinkedInputPoseNode;
};
void UAnimGraphNode_LinkedInputPose::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)
{
IDetailCategoryBuilder& InputsCategoryBuilder = DetailBuilder.EditCategory("Inputs");
TArray<TWeakObjectPtr<UObject>> OuterObjects;
DetailBuilder.GetObjectsBeingCustomized(OuterObjects);
if(OuterObjects.Num() != 1)
{
InputsCategoryBuilder.SetCategoryVisibility(false);
return;
}
// skip if we cant edit this node as it is an interface graph
UAnimGraphNode_LinkedInputPose* OuterNode = CastChecked<UAnimGraphNode_LinkedInputPose>(OuterObjects[0].Get());
if(!OuterNode->CanUserDeleteNode())
{
FText ReadOnlyWarning = LOCTEXT("ReadOnlyWarning", "This input pose is read-only and cannot be edited");
InputsCategoryBuilder.SetCategoryVisibility(false);
IDetailCategoryBuilder& WarningCategoryBuilder = DetailBuilder.EditCategory("InputPose", LOCTEXT("InputPoseCategory", "Input Pose"));
WarningCategoryBuilder.AddCustomRow(ReadOnlyWarning)
.WholeRowContent()
[
SNew(STextBlock)
.Text(ReadOnlyWarning)
.Font(IDetailLayoutBuilder::GetDetailFont())
];
return;
}
TSharedPtr<IPropertyHandle> NamePropertyHandle = DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UAnimGraphNode_LinkedInputPose, Node.Name), GetClass());
InputsCategoryBuilder.AddProperty(NamePropertyHandle)
.OverrideResetToDefault(FResetToDefaultOverride::Hide())
.CustomWidget()
.NameContent()
[
NamePropertyHandle->CreatePropertyNameWidget()
]
.ValueContent()
[
MakeNameWidget(DetailBuilder)
];
InputsCategoryBuilder.AddProperty(GET_MEMBER_NAME_CHECKED(UAnimGraphNode_LinkedInputPose, Inputs), GetClass())
.ShouldAutoExpand(true);
}
TSharedRef<SWidget> UAnimGraphNode_LinkedInputPose::MakeNameWidget(IDetailLayoutBuilder& DetailBuilder)
{
TSharedPtr<IPropertyHandle> NamePropertyHandle = DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UAnimGraphNode_LinkedInputPose, Node.Name), GetClass());
return SNew(SLinkedInputPoseNodeLabelWidget, NamePropertyHandle, this);
}
bool UAnimGraphNode_LinkedInputPose::HasExternalDependencies(TArray<UStruct*>* OptionalOutput) const
{
const UBlueprint* SourceBlueprint = GetBlueprint();
UClass* SourceClass = FunctionReference.GetMemberParentClass(GetBlueprintClassFromNode());
bool bResult = (SourceClass != nullptr) && (SourceClass->ClassGeneratedBy != SourceBlueprint);
if (bResult && OptionalOutput)
{
OptionalOutput->AddUnique(SourceClass);
}
const bool bSuperResult = Super::HasExternalDependencies(OptionalOutput);
return bSuperResult || bResult;
}
int32 UAnimGraphNode_LinkedInputPose::GetNumInputs() const
{
if (UFunction* Function = FunctionReference.ResolveMember<UFunction>(GetBlueprintClassFromNode()))
{
// Count the inputs from parameters.
int32 NumParameters = 0;
IterateFunctionParameters([&NumParameters](const FName& InName, const FEdGraphPinType& InPinType)
{
if(!UAnimationGraphSchema::IsPosePin(InPinType))
{
NumParameters++;
}
});
return NumParameters;
}
else
{
return Inputs.Num();
}
}
void UAnimGraphNode_LinkedInputPose::PromoteFromInterfaceOverride()
{
if (UFunction* Function = FunctionReference.ResolveMember<UFunction>(GetBlueprintClassFromNode()))
{
IterateFunctionParameters([this](const FName& InName, const FEdGraphPinType& InPinType)
{
if(!UAnimationGraphSchema::IsPosePin(InPinType))
{
Inputs.Emplace(InName, InPinType);
}
});
// Remove the signature class now, that is not relevant.
FunctionReference.SetSelfMember(FunctionReference.GetMemberName());
InputPoseIndex = INDEX_NONE;
}
}
void UAnimGraphNode_LinkedInputPose::IterateFunctionParameters(TFunctionRef<void(const FName&, const FEdGraphPinType&)> InFunc) const
{
if (UFunction* Function = FunctionReference.ResolveMember<UFunction>(GetBlueprintClassFromNode()))
{
const UEdGraphSchema_K2* K2Schema = GetDefault<UEdGraphSchema_K2>();
// if the generated class is not up to date, use the skeleton's class function to create pins:
Function = FBlueprintEditorUtils::GetMostUpToDateFunction(Function);
// We need to find all parameters AFTER the pose we are representing
int32 CurrentPoseIndex = 0;
FProperty* PoseParam = nullptr;
for (TFieldIterator<FProperty> PropIt(Function); PropIt && (PropIt->PropertyFlags & CPF_Parm); ++PropIt)
{
FProperty* Param = *PropIt;
const bool bIsFunctionInput = !Param->HasAnyPropertyFlags(CPF_OutParm) || Param->HasAnyPropertyFlags(CPF_ReferenceParm);
if (bIsFunctionInput)
{
FEdGraphPinType PinType;
if(K2Schema->ConvertPropertyToPinType(Param, PinType))
{
if(PoseParam == nullptr)
{
if(UAnimationGraphSchema::IsPosePin(PinType))
{
if(CurrentPoseIndex == InputPoseIndex)
{
PoseParam = Param;
InFunc(Param->GetFName(), PinType);
}
CurrentPoseIndex++;
}
}
else
{
if(UAnimationGraphSchema::IsPosePin(PinType))
{
// Found next pose param, so exit
break;
}
else
{
InFunc(Param->GetFName(), PinType);
}
}
}
}
}
}
else
{
// First call pose
InFunc(Node.Name, UAnimationGraphSchema::MakeLocalSpacePosePin());
// Then each input
for(const FAnimBlueprintFunctionPinInfo& PinInfo : Inputs)
{
InFunc(PinInfo.Name, PinInfo.Type);
}
}
}
bool UAnimGraphNode_LinkedInputPose::IsCompatibleWithGraph(UEdGraph const* Graph) const
{
return Graph->GetFName() == UEdGraphSchema_K2::GN_AnimGraph;
}
void UAnimGraphNode_LinkedInputPose::GetOutputLinkAttributes(FNodeAttributeArray& OutAttributes) const
{
// We have the potential to output ALL registered attributes
const UAnimGraphAttributes* AnimGraphAttributes = GetDefault<UAnimGraphAttributes>();
AnimGraphAttributes->ForEachAttribute([&OutAttributes](const FAnimGraphAttributeDesc& InDesc)
{
OutAttributes.Add(InDesc.Name);
});
}
#undef LOCTEXT_NAMESPACE