Custom Anim Node: problem with getting output pin to output data

I have been making a custom anim node based on the BlendByListEnum node. My goal is to create a system that can easily swap between weapon types, so I don’t have to make separate states just for all the different combos. Furthermore, due to how my current implementation works, I set a timer every time an attack is started based on the duration of said attack, so that when the attack is finished, I can take care of various things such as enabling the character to move, continue a combo and whatnot. Long story short, for this I also need the length of the animation.

Now, my problem is as follows: while it seems like I am updating the AnimLength variable in the AnimNode properly, the data never actually seems to leave the node. No matter what I do, when I try to use the value in the following StartCombo node, it always becomes 0.

The code I used is as follows (I should not that the animgraphnode code is largely the blendbylistenum graphnode code with some changes to accommodate my own variables):

AnimNode.h

USTRUCT()
struct FPA_AnimNode_BlendListByEnum : public FAnimNode_BlendListBase
{
	GENERATED_USTRUCT_BODY()

public:
	UPROPERTY()
		TArray<int32> EnumToPoseIndex;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Runtime, meta = (AlwaysAsPin))
		mutable uint8 ActiveEnumValue;

	UPROPERTY(EditAnywhere, EditFixedSize, BlueprintReadWrite, Category = Config, meta = (PinShownByDefault))
		TArray<float> AnimLengthList;

	UPROPERTY()
		float AnimLength;

protected:
	virtual int32 GetActiveChildIndex() override;
	virtual FString GetNodeName(FNodeDebugData& DebugData) override { return DebugData.GetNodeName(this); }

public:
	virtual void Initialize(const FAnimationInitializeContext& Context) override;
	virtual void Update(const FAnimationUpdateContext& Context) override;
	virtual void Evaluate(FPoseContext& Output) override;

#if WITH_EDITOR
	virtual void AddPose()
	{
		BlendTime.Add(0.1f);
		AnimLengthList.Add(0.f);
		new (BlendPose) FPoseLink();
	}

	virtual void RemovePose(int32 PoseIndex)
	{
		BlendTime.RemoveAt(PoseIndex);
		AnimLengthList.RemoveAt(PoseIndex);
		BlendPose.RemoveAt(PoseIndex);
	}
#endif
};

AnimNode.cpp

int32 FPA_AnimNode_BlendListByEnum::GetActiveChildIndex()
{
	if (EnumToPoseIndex.IsValidIndex(ActiveEnumValue))
	{
		return EnumToPoseIndex[ActiveEnumValue];
	}
	else
	{
		return 0;
	}
}

void FPA_AnimNode_BlendListByEnum::Initialize(const FAnimationInitializeContext& Context) 
{
	const int NumPoses = BlendPose.Num();
	checkSlow(AnimLengthList.Num() == NumPoses);

	if (NumPoses > 0)
	{
		const int32 ChildIndex = GetActiveChildIndex();

		EvaluateGraphExposedInputs.Execute(Context);
		AnimLength = AnimLengthList[ChildIndex];
	}

	FAnimNode_BlendListBase::Initialize(Context);
}

void FPA_AnimNode_BlendListByEnum::Update(const FAnimationUpdateContext& Context)
{
	const int NumPoses = BlendPose.Num();
	checkSlow(AnimLengthList.Num() == NumPoses);

	if (NumPoses > 0)
	{
		const int32 ChildIndex = GetActiveChildIndex();

		EvaluateGraphExposedInputs.Execute(Context);
		AnimLength = AnimLengthList[ChildIndex];
	}

	FAnimNode_BlendListBase::Update(Context);
}

void FPA_AnimNode_BlendListByEnum::Evaluate(FPoseContext& Output)
{
	const int NumPoses = BlendPose.Num();
	checkSlow(AnimLengthList.Num() == NumPoses);

	if (NumPoses > 0)
	{
		const int32 ChildIndex = GetActiveChildIndex();
		
		AnimLength = AnimLengthList[ChildIndex];
		EvaluateGraphExposedInputs.Execute(Output);

		//GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, FString::SanitizeFloat(AnimLength));
	}

	FAnimNode_BlendListBase::Evaluate(Output);
}

AnimGraphNode.h

UCLASS()
class MYGAME_API UPA_AnimGraphNode_BlendListByEnum : public UAnimGraphNode_BlendListBase, public INodeDependingOnEnumInterface
{
	GENERATED_BODY()

	UPROPERTY(EditAnywhere, Category = Settings)
		FPA_AnimNode_BlendListByEnum Node;

	UPA_AnimGraphNode_BlendListByEnum();

protected:
	/** Name of the enum being switched on */
	UPROPERTY()
		UEnum* BoundEnum;

	UPROPERTY()
		TArray<FName> VisibleEnumEntries;
public:
	//my own override
	virtual void CreateOutputPins() override;

	// UEdGraphNode interface
	virtual FText GetTooltipText() const override;
	virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override;
	virtual void PostPlacedNewNode() override;
	virtual void Serialize(FArchive& Ar) override;
	// End of UEdGraphNode interface

	// UK2Node interface
	virtual void GetContextMenuActions(const FGraphNodeContextMenuBuilder& Context) const override;
	// End of UK2Node interface

	// UAnimGraphNode_Base interface
	virtual FString GetNodeCategory() const override;
	virtual void GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const override;
	virtual void CustomizePinData(UEdGraphPin* Pin, FName SourcePropertyName, int32 ArrayIndex) const override;
	virtual void ValidateAnimNodeDuringCompilation(class USkeleton* ForSkeleton, class FCompilerResultsLog& MessageLog) override;
	virtual void BakeDataDuringCompilation(class FCompilerResultsLog& MessageLog) override;
	virtual void PreloadRequiredAssets() override;
	// End of UAnimGraphNode_Base interface

	//@TODO: Generalize this behavior (returning a list of actions/delegates maybe?)
	virtual void RemovePinFromBlendList(UEdGraphPin* Pin);

	// INodeDependingOnEnumInterface
	virtual class UEnum* GetEnum() const override { return BoundEnum; }
	virtual bool ShouldBeReconstructedAfterEnumChanged() const override { return true; }
	// End of INodeDependingOnEnumInterface
protected:
	// Exposes a pin corresponding to the specified element name
	void ExposeEnumElementAsPin(FName EnumElementName);

	// Gets information about the specified pin.  If both bIsPosePin and bIsTimePin are false, the index is meaningless
	static void GetCustomPinInformation(const FString& InPinName, int32& Out_PinIndex, bool& Out_bIsPosePin, bool& Out_bIsTimePin, bool& Out_bIsAnimPin);

private:
	/** Constructing FText strings can be costly, so we cache the node's title */
	FNodeTextCache CachedNodeTitle;
	
	
};

AnimGraphNode.cpp

UPA_AnimGraphNode_BlendListByEnum::UPA_AnimGraphNode_BlendListByEnum()
{

}

FString UPA_AnimGraphNode_BlendListByEnum::GetNodeCategory() const
{
	return TEXT("Attacks");
	//return FString::Printf(TEXT("%s, Blend List by enum and return animation"), *Super::GetNodeCategory());
}

FText UPA_AnimGraphNode_BlendListByEnum::GetTooltipText() const
{
	// FText::Format() is slow, so we utilize the cached list title
	return GetNodeTitle(ENodeTitleType::ListView);
}

FText UPA_AnimGraphNode_BlendListByEnum::GetNodeTitle(ENodeTitleType::Type TitleType) const
{
	if (BoundEnum == nullptr)
	{
		return LOCTEXT("AnimGraphNode_BlendListByEnum_TitleError", "ERROR: Blend Poses Return Animation (by missing enum)");
	}
	// @TODO: don't know enough about this node type to comfortably assert that
	//        the BoundEnum won't change after the node has spawned... until
	//        then, we'll leave this optimization off
	else //if (CachedNodeTitle.IsOutOfDate(this))
	{
		FFormatNamedArguments Args;
		Args.Add(TEXT("EnumName"), FText::FromString(BoundEnum->GetName()));
		// FText::Format() is slow, so we cache this to save on performance
		CachedNodeTitle.SetCachedText(FText::Format(LOCTEXT("AnimGraphNode_BlendListByEnum_Title", "Blend Poses Return Animation ({EnumName})"), Args), this);
	}
	return CachedNodeTitle;
}

void UPA_AnimGraphNode_BlendListByEnum::CreateOutputPins()
{
	Super::CreateOutputPins();

	const UAnimationGraphSchema* Schema = GetDefault<UAnimationGraphSchema>();
	CreatePin(EGPD_Output, Schema->PC_Float, TEXT(""), NULL, /*bIsArray*/ false, /*bIsReference*/ false, TEXT("AnimLength"));
}

void UPA_AnimGraphNode_BlendListByEnum::PostPlacedNewNode()
{
	// Make sure we start out with a pin
	Node.AddPose();
}

void UPA_AnimGraphNode_BlendListByEnum::GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const
{
	struct GetMenuActions_Utils
	{
		static void SetNodeEnum(UEdGraphNode* NewNode, bool /*bIsTemplateNode*/, TWeakObjectPtr<UEnum> NonConstEnumPtr)
		{
			UPA_AnimGraphNode_BlendListByEnum* BlendListEnumNode = CastChecked<UPA_AnimGraphNode_BlendListByEnum>(NewNode);
			BlendListEnumNode->BoundEnum = NonConstEnumPtr.Get();
		}
	};

	UClass* NodeClass = GetClass();
	// add all blendlist enum entries
	ActionRegistrar.RegisterEnumActions(FBlueprintActionDatabaseRegistrar::FMakeEnumSpawnerDelegate::CreateLambda([NodeClass](const UEnum* Enum)->UBlueprintNodeSpawner*
	{
		UBlueprintNodeSpawner* NodeSpawner = UBlueprintNodeSpawner::Create(NodeClass);
		check(NodeSpawner != nullptr);
		TWeakObjectPtr<UEnum> NonConstEnumPtr = Enum;
		NodeSpawner->CustomizeNodeDelegate = UBlueprintNodeSpawner::FCustomizeNodeDelegate::CreateStatic(GetMenuActions_Utils::SetNodeEnum, NonConstEnumPtr);

		return NodeSpawner;
	}));
}

void UPA_AnimGraphNode_BlendListByEnum::GetContextMenuActions(const FGraphNodeContextMenuBuilder& Context) const
{
	if (!Context.bIsDebugging && (BoundEnum != NULL))
	{
		if ((Context.Pin != NULL) && (Context.Pin->Direction == EGPD_Input))
		{
			int32 RawArrayIndex = 0;
			bool bIsPosePin = false;
			bool bIsTimePin = false;
			bool bIsAnimPin = false;

			GetCustomPinInformation(Context.Pin->PinName, /*out*/ RawArrayIndex, /*out*/ bIsPosePin, /*out*/ bIsTimePin, /*out*/ bIsAnimPin);

			if (bIsPosePin || bIsTimePin || bIsAnimPin)
			{
				const int32 ExposedEnumIndex = RawArrayIndex - 1;

				if (ExposedEnumIndex != INDEX_NONE)
				{
					// Offer to remove this specific pin
					FUIAction Action = FUIAction(FExecuteAction::CreateUObject(this, &UPA_AnimGraphNode_BlendListByEnum::RemovePinFromBlendList, const_cast<UEdGraphPin*>(Context.Pin)));
					Context.MenuBuilder->AddMenuEntry(LOCTEXT("RemovePose", "Remove Pose"), FText::GetEmpty(), FSlateIcon(), Action);
				}
			}
		}

		// Offer to add any not-currently-visible pins
		bool bAddedHeader = false;
		const int32 MaxIndex = BoundEnum->NumEnums() - 1; // we don't want to show _MAX enum
		for (int32 Index = 0; Index < MaxIndex; ++Index)
		{
			FName ElementName = BoundEnum->GetEnum(Index);
			if (!VisibleEnumEntries.Contains(ElementName))
			{
				FText PrettyElementName = BoundEnum->GetEnumText(Index);

				// Offer to add this entry
				if (!bAddedHeader)
				{
					bAddedHeader = true;
					Context.MenuBuilder->BeginSection("AnimGraphNodeAddElementPin", LOCTEXT("ExposeHeader", "Add pin for element"));
					{
						FUIAction Action = FUIAction(FExecuteAction::CreateUObject(this, &UPA_AnimGraphNode_BlendListByEnum::ExposeEnumElementAsPin, ElementName));
						Context.MenuBuilder->AddMenuEntry(PrettyElementName, PrettyElementName, FSlateIcon(), Action);
					}
					Context.MenuBuilder->EndSection();
				}
				else
				{
					FUIAction Action = FUIAction(FExecuteAction::CreateUObject(this, &UPA_AnimGraphNode_BlendListByEnum::ExposeEnumElementAsPin, ElementName));
					Context.MenuBuilder->AddMenuEntry(PrettyElementName, PrettyElementName, FSlateIcon(), Action);
				}
			}
		}
	}
}

void UPA_AnimGraphNode_BlendListByEnum::ExposeEnumElementAsPin(FName EnumElementName)
{
	if (!VisibleEnumEntries.Contains(EnumElementName))
	{
		FScopedTransaction Transaction(LOCTEXT("ExposeElement", "ExposeElement"));
		Modify();

		VisibleEnumEntries.Add(EnumElementName);

		Node.AddPose();

		ReconstructNode();

		FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(GetBlueprint());
	}
}

void UPA_AnimGraphNode_BlendListByEnum::RemovePinFromBlendList(UEdGraphPin* Pin)
{
	int32 RawArrayIndex = 0;
	bool bIsPosePin = false;
	bool bIsTimePin = false;
	bool bIsAnimPin = false;

	GetCustomPinInformation(Pin->PinName, /*out*/ RawArrayIndex, /*out*/ bIsPosePin, /*out*/ bIsTimePin, /*out*/ bIsAnimPin);

	const int32 ExposedEnumIndex = (bIsPosePin || bIsTimePin || bIsAnimPin) ? (RawArrayIndex - 1) : INDEX_NONE;

	if (ExposedEnumIndex != INDEX_NONE)
	{
		FScopedTransaction Transaction(LOCTEXT("RemovePin", "RemovePin"));
		Modify();

		// Record it as no longer exposed
		VisibleEnumEntries.RemoveAt(ExposedEnumIndex);

		// Remove the pose from the node
		UProperty* AssociatedProperty;
		int32 ArrayIndex;
		GetPinAssociatedProperty(GetFNodeType(), Pin, /*out*/ AssociatedProperty, /*out*/ ArrayIndex);

		ensure(ArrayIndex == (ExposedEnumIndex + 1));

		// setting up removed pins info 
		RemovedPinArrayIndex = ArrayIndex;
		Node.RemovePose(ArrayIndex);
		ReconstructNode();
		//@TODO: Just want to invalidate the visual representation currently
		FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(GetBlueprint());
	}
}

void UPA_AnimGraphNode_BlendListByEnum::GetCustomPinInformation(const FString& InPinName, int32& Out_PinIndex, bool& Out_bIsPosePin, bool& Out_bIsTimePin, bool& Out_bIsAnimPin)
{
	const int32 UnderscoreIndex = InPinName.Find(TEXT("_"));
	if (UnderscoreIndex != INDEX_NONE)
	{
		const FString ArrayName = InPinName.Left(UnderscoreIndex);
		Out_PinIndex = FCString::Atoi(*(InPinName.Mid(UnderscoreIndex + 1)));

		Out_bIsPosePin = ArrayName == TEXT("BlendPose");
		Out_bIsTimePin = ArrayName == TEXT("BlendTime");
		Out_bIsAnimPin = ArrayName == TEXT("AnimLengthList");
	}
	else
	{
		Out_bIsPosePin = false;
		Out_bIsTimePin = false;
		Out_bIsAnimPin = false;
		Out_PinIndex = INDEX_NONE;
	}
}

void UPA_AnimGraphNode_BlendListByEnum::CustomizePinData(UEdGraphPin* Pin, FName SourcePropertyName, int32 ArrayIndex) const
{
	// if pin name starts with BlendPose or BlendWeight, change to enum name 
	bool bIsPosePin;
	bool bIsTimePin;
	bool bIsAnimPin;

	int32 RawArrayIndex;
	GetCustomPinInformation(Pin->PinName, /*out*/ RawArrayIndex, /*out*/ bIsPosePin, /*out*/ bIsTimePin, /*out*/ bIsAnimPin);
	checkSlow(RawArrayIndex == ArrayIndex);

	if (bIsPosePin || bIsTimePin || bIsAnimPin)
	{
		if (RawArrayIndex > 0)
		{
			const int32 ExposedEnumPinIndex = RawArrayIndex - 1;

			// find pose index and see if it's mapped already or not
			if (VisibleEnumEntries.IsValidIndex(ExposedEnumPinIndex) && (BoundEnum != NULL))
			{
				const FName& EnumElementName = VisibleEnumEntries[ExposedEnumPinIndex];
				const int32 EnumIndex = BoundEnum->FindEnumIndex(EnumElementName);
				if (EnumIndex != INDEX_NONE)
				{
					Pin->PinFriendlyName = BoundEnum->GetEnumText(EnumIndex);
				}
				else
				{
					Pin->PinFriendlyName = FText::FromName(EnumElementName);
				}
			}
			else
			{
				Pin->PinFriendlyName = LOCTEXT("InvalidIndex", "Invalid index");
			}
		}
		else if (ensure(RawArrayIndex == 0))
		{
			Pin->PinFriendlyName = LOCTEXT("Default", "Default");
		}

		// Append the pin type
		if (bIsPosePin)
		{
			FFormatNamedArguments Args;
			Args.Add(TEXT("PinFriendlyName"), Pin->PinFriendlyName);
			Pin->PinFriendlyName = FText::Format(LOCTEXT("FriendlyNamePose", "{PinFriendlyName} Pose"), Args);
		}

		if (bIsTimePin)
		{
			FFormatNamedArguments Args;
			Args.Add(TEXT("PinFriendlyName"), Pin->PinFriendlyName);
			Pin->PinFriendlyName = FText::Format(LOCTEXT("FriendlyNameBlendTime", "{PinFriendlyName} Blend Time"), Args);
		}

		if (bIsAnimPin)
		{
			FFormatNamedArguments Args;
			Args.Add(TEXT("PinFriendlyName"), Pin->PinFriendlyName);
			Pin->PinFriendlyName = FText::Format(LOCTEXT("FriendlyNameAnimLength", "{PinFriendlyName} Anim Length"), Args);
		}
	}
}

void UPA_AnimGraphNode_BlendListByEnum::Serialize(FArchive& Ar)
{
	Super::Serialize(Ar);

	if (Ar.IsLoading())
	{
		if (BoundEnum != NULL)
		{
			PreloadObject(BoundEnum);

			for (auto ExposedIt = VisibleEnumEntries.CreateIterator(); ExposedIt; ++ExposedIt)
			{
				FName& EnumElementName = *ExposedIt;
				const int32 EnumIndex = BoundEnum->FindEnumIndex(EnumElementName);

				if (EnumIndex != INDEX_NONE)
				{
					// This handles redirectors, we need to update the VisibleEnumEntries if the name has changed
					FName NewElementName = BoundEnum->GetEnum(EnumIndex);

					if (NewElementName != EnumElementName)
					{
						EnumElementName = NewElementName;
					}
				}
			}
		}
	}
}

void UPA_AnimGraphNode_BlendListByEnum::ValidateAnimNodeDuringCompilation(class USkeleton* ForSkeleton, class FCompilerResultsLog& MessageLog)
{
	if (BoundEnum == NULL)
	{
		MessageLog.Error(TEXT("@@ references an unknown enum; please delete the node and recreate it"), this);
	}
}

void UPA_AnimGraphNode_BlendListByEnum::PreloadRequiredAssets()
{
	PreloadObject(BoundEnum);

	Super::PreloadRequiredAssets();
}

void UPA_AnimGraphNode_BlendListByEnum::BakeDataDuringCompilation(class FCompilerResultsLog& MessageLog)
{
	if (BoundEnum != NULL)
	{
		PreloadObject(BoundEnum);

		// Zero the array out so it looks up the default value, and stat counting at index 1
		Node.EnumToPoseIndex.Empty();
		Node.EnumToPoseIndex.AddZeroed(BoundEnum->NumEnums());
		int32 PinIndex = 1;

		// Run thru the enum entries
		for (auto ExposedIt = VisibleEnumEntries.CreateConstIterator(); ExposedIt; ++ExposedIt)
		{
			const FName& EnumElementName = *ExposedIt;
			const int32 EnumIndex = BoundEnum->FindEnumIndex(EnumElementName);

			if (EnumIndex != INDEX_NONE)
			{
				Node.EnumToPoseIndex[EnumIndex] = PinIndex;
			}
			else
			{
				MessageLog.Error(*FString::Printf(TEXT("@@ references an unknown enum entry %s"), *EnumElementName.ToString()), this);
			}

			++PinIndex;
		}
	}
}

How could you drive class from “AnimNodeBase.h”? I can’t find it in the C++ class creation panel.

I also meet this problem, anybody knows how to fix it?

I’m facing the same problem myself on 4.21. OutputContext.Curve.Set does not seem to be setting anything to the model. I can see my values in my code. But the model remains static.

I’ve also tried to accomplish this to no avail.

It’d be great if someone could figure this out since anim node are like blackboxes; we can’t get any data out of them, other than a pose.