Blend list by gameplay tag

In the animation graph we have a blend poses by int, bool, enum. Could Epic provide a blend poses by GameplayTag?

Our use case seems pretty straight forwards. I have a gameplay tag that can drive a blend pose.

Thank you for the consideration

Russell

[Image Removed]

[Attachment Removed]

Hey there,

Thanks for the suggestion. The team probably won’t be able to add this, but this is definitely something that you could add very easily. Subclassing FAnimNode_BlendListBase and making an FAnimNode_BlendListByGameplayTag should be very straightforward, and it wouldn’t touch engine code modification.

Let me know if you would like something cursory to try. The unfortunate bit would be that it would need to be something you maintain or expand upon.

Dustin

[Attachment Removed]

Thanks Dustin,

I did start to reverse engineer the BlendByEnum as it also named the pins on the graph node. I don’t have everything completed yet, and also don’t pretend to know how everything works, it appears to be working in PIE. I did throw everything, including the kitchen sink, in here to solve some problems so I need to check out my includes and such. [Image Removed]

#pragma once
 
// Epic Includes
#include "AnimNodes/AnimNode_BlendListBase.h"
#include "GameplayTagContainer.h"
 
// My Includes
#include "AnimNode_BlendByGameplayTag.generated.h"
 
//=============================================================================
// FAnimNode_BlendByGameplayTag
//=============================================================================
// This is the runtime portion for an animation graph node that will hide
// multiple bones by scaling them to FVector::ZeroVector. The bones are listed in an array within a struct.
//=============================================================================
 
USTRUCT(BlueprintInternalUseOnly)
struct My_API FAnimNode_BlendByGameplayTag : public FAnimNode_BlendListBase
{
	GENERATED_BODY()
	
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// @BeginSection FAnimNode_Base
	
// @EndSection FAnimNode_Base
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// @BeginSection FAnimNode_BlendByGameplayTag
 
private:
	// Mapping from enum value to BlendPose index; there will be one entry per entry in the enum; entries out of range always map to pose index 0
	UPROPERTY(meta=(FoldProperty))
	TArray<FGameplayTag> GameplayTagToPoseArray;
 
public:
	UPROPERTY(EditAnywhere,  Category=Runtime, meta=(PinShownByDefault, FoldProperty))
	FGameplayTag ActiveGameplayTag;
	
	UPROPERTY(EditAnywhere,  Category=Runtime, meta=(FoldProperty, NeverAsPin))
	FGameplayTagContainer AnimNode_GameplayTagContainer;
 
	FAnimNode_BlendByGameplayTag() = default;
 
	FGameplayTagContainer GetGameplayTagContainer() { return AnimNode_GameplayTagContainer; }
	
 
	// Get the mapping from gameplay tag value to BlendPose index;
	// there will be one entry per entry in the gameplay tag list;
	// entries out of range always map to pose index 0
	const TArray<FGameplayTag>& GetGameplayTagToPoseIndex() const;
 
	void SetGameplayTagToPoseArray(const TArray<FGameplayTag>& InArray) { GameplayTagToPoseArray = InArray; }
	// Get the currently selected pose (as a gameplay tag value)
	FGameplayTag GetActiveGameplayTagValue() const;
	
protected:	
	virtual int32 GetActiveChildIndex() override;
	virtual FString GetNodeName(FNodeDebugData& DebugData) override { return DebugData.GetNodeName(this); }
	
// @EndSection FAnimNode_BlendByGameplayTag
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	
};

[Attachment Removed]

#include "Animation/AnimNode/AnimNode_BlendByGameplayTag.h"
#include "Animation/AnimNodeBase.h"
 
#include UE_INLINE_GENERATED_CPP_BY_NAME(AnimNode_BlendByGameplayTag)
 
const TArray<FGameplayTag>& FAnimNode_BlendByGameplayTag::GetGameplayTagToPoseIndex() const
{
	return GET_ANIM_NODE_DATA(TArray<FGameplayTag>, GameplayTagToPoseArray);
}
 
FGameplayTag FAnimNode_BlendByGameplayTag::GetActiveGameplayTagValue() const
{
	return GET_ANIM_NODE_DATA(FGameplayTag, ActiveGameplayTag);
}
 
int32 FAnimNode_BlendByGameplayTag::GetActiveChildIndex()
{
	const FGameplayTag CurrentActiveEnumValue = GetActiveGameplayTagValue();
	const TArray<FGameplayTag>& CurrentEnumToPoseIndex = GetGameplayTagToPoseIndex();
	if (CurrentEnumToPoseIndex.Contains(CurrentActiveEnumValue))
	{
		// Add one to account for default pose.
		return CurrentEnumToPoseIndex.Find(CurrentActiveEnumValue) + 1;
	}
	
	return 0;
}
#pragma once
 
// Epic Includes
#include "CoreMinimal.h"
#include "GameplayTagContainer.h"
 
// My Includes
#include "AnimGraphNode_BlendListBase.h"
#include "Animation/AnimNode/AnimNode_BlendByGameplayTag.h"
#include "AnimGraphNode_BlendByGameplayTag.generated.h"
 
//=============================================================================
// UAnimGraphNode_BlendByGameplayTag
//=============================================================================
// This is the editor portion for an animation graph node that will
// blend through multiple poses by use of a gameplay tag 
//=============================================================================
 
UCLASS(MinimalAPI, meta=(Keywords = "Blend By Gameplay Tag"))
class UAnimGraphNode_BlendByGameplayTag : public UAnimGraphNode_BlendListBase
{
	GENERATED_UCLASS_BODY()
 
	UPROPERTY(EditAnywhere, Category=Settings)
	FAnimNode_BlendByGameplayTag Node;
 
protected:
	UPROPERTY()
	TArray<FGameplayTag> ActiveTagEntries;
	
public:
	virtual void PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) override;
	
	// UK2Node interface
	virtual void GetNodeContextMenuActions(class UToolMenu* Menu, class UGraphNodeContextMenuContext* Context) const override;
	// End of UK2Node interface
	
	// UAnimGraphNode_Base interface
	virtual FEditorModeID GetEditorMode() const override;	
	virtual void BakeDataDuringCompilation(class FCompilerResultsLog& MessageLog) override;
	virtual void CopyNodeDataToPreviewNode(FAnimNode_Base* InPreviewNode) override;
	static void GetPinInformation(const FString& InPinName, int32& Out_PinIndex, bool& Out_bIsPosePin, bool& Out_bIsTimePin);
	virtual void CustomizePinData(UEdGraphPin* Pin, FName SourcePropertyName, int32 ArrayIndex) const override;
	// End of UAnimGraphNode_Base interface
	
	// UEdGraphNode interface
	virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override;
	virtual FText GetTooltipText() const override;
	// End of UEdGraphNode interface
 
	void RebuildPoseIndexList();
	
private:
	/** Constructing FText strings can be costly, so we cache the node's title */
	FNodeTitleTextTable CachedNodeTitles;
};
 

[Attachment Removed]

// Epic Includes
#include "Animation/AnimNode/AnimGraphNode_BlendByGameplayTag.h"
#include "Kismet2/CompilerResultsLog.h"
#include "GameplayTagContainer.h"
#include "AnimNodeEditModes.h"
#include "Textures/SlateIcon.h"
#include "Framework/Commands/UIAction.h"
#include "ToolMenus.h"
#include "Kismet2/BlueprintEditorUtils.h"
 
#include "ScopedTransaction.h"
#include "Kismet2/CompilerResultsLog.h"
 
// My Includes
#include "Animation/AnimNode/AnimNode_BlendByGameplayTag.h"
 
#define LOCTEXT_NAMESPACE "A3Nodes"
 
 
UAnimGraphNode_BlendByGameplayTag::UAnimGraphNode_BlendByGameplayTag(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	// Make sure we start out with a pin
	Node.AddPose();
}
 
FText UAnimGraphNode_BlendByGameplayTag::GetTooltipText() const
{
	return LOCTEXT("AnimGraphNode_BlendByGameplayTag_Tooltip",
		"This node blends poses based on an input gameplay tag.");
}
 
void UAnimGraphNode_BlendByGameplayTag::RebuildPoseIndexList()
{
	FScopedTransaction Transaction( NSLOCTEXT("A3Nodes", "AddBlendListPin", "AddBlendListPin") );
	
	Modify();
 
	const FGameplayTagContainer& ActiveTagContainer = Node.GetGameplayTagContainer();
 
	// Remove old gameplay tags.
	for(int32 idx = ActiveTagEntries.Num() - 1; idx > 0; --idx)
	{
		FGameplayTag ActiveTag = ActiveTagEntries[idx];
		if (!ActiveTagContainer.HasTag(ActiveTag))
		{
			// Remove inactive indicies
			Node.RemovePose(idx);
			ActiveTagEntries.RemoveAt(idx);
		}
	}
 
	// Add new gameplay tag pose indicies
	for (FGameplayTag NewTag : ActiveTagContainer)
	{
		if (ActiveTagEntries.Contains(NewTag))
		{
			continue;
		}
		ActiveTagEntries.Add(NewTag);
		Node.AddPose();
	}
	
	ReconstructNode();
 
	FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(GetBlueprint());
}
 
FText UAnimGraphNode_BlendByGameplayTag::GetNodeTitle(ENodeTitleType::Type TitleType) const
{
	return LOCTEXT("AnimGraphNode_BlendByGameplayTag_Title", "Blend Poses by Gameplay Tag");
}
 
void UAnimGraphNode_BlendByGameplayTag::PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent)
{
	Super::PostEditChangeProperty(PropertyChangedEvent);
		
	RebuildPoseIndexList();
}
 
void UAnimGraphNode_BlendByGameplayTag::GetNodeContextMenuActions(class UToolMenu* Menu,
	class UGraphNodeContextMenuContext* Context) const
{
	Super::GetNodeContextMenuActions(Menu, Context);
 
	// Offer to add any not-currently-visible pins
	FToolMenuSection* Section = nullptr;
	// Offer to add this entry
	if (!Section)
	{
		Section = &Menu->AddSection("AnimGraphNodeAddElementPin", LOCTEXT("ExposeHeader", "Add pin for element"));
	}
 
	FText ElementName = FText::FromString("Rebuild List");
	FUIAction Action = FUIAction( FExecuteAction::CreateUObject( const_cast<UAnimGraphNode_BlendByGameplayTag*>(this), &UAnimGraphNode_BlendByGameplayTag::RebuildPoseIndexList));
	Section->AddMenuEntry(NAME_None, ElementName, ElementName, FSlateIcon(), Action);
}
 
FEditorModeID UAnimGraphNode_BlendByGameplayTag::GetEditorMode() const
{
	return AnimNodeEditModes::AnimNode;
}
 
void UAnimGraphNode_BlendByGameplayTag::BakeDataDuringCompilation(class FCompilerResultsLog& MessageLog)
{
	Super::BakeDataDuringCompilation(MessageLog);
 
	Node.SetGameplayTagToPoseArray(ActiveTagEntries);	
}
 
void UAnimGraphNode_BlendByGameplayTag::CopyNodeDataToPreviewNode(FAnimNode_Base* InPreviewNode)
{
	FAnimNode_BlendByGameplayTag* PreviewNode = static_cast<FAnimNode_BlendByGameplayTag*>(InPreviewNode);
}
 
void UAnimGraphNode_BlendByGameplayTag::GetPinInformation(const FString& InPinName, int32& Out_PinIndex, bool& Out_bIsPosePin, bool& Out_bIsTimePin)
{
	const int32 UnderscoreIndex = InPinName.Find(TEXT("_"), ESearchCase::CaseSensitive);
	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"));
	}
	else
	{
		Out_bIsPosePin = false;
		Out_bIsTimePin = false;
		Out_PinIndex = INDEX_NONE;
	}
}
 
void UAnimGraphNode_BlendByGameplayTag::CustomizePinData(UEdGraphPin* Pin, FName SourcePropertyName, int32 ArrayIndex) const
{
	// if pin name starts with BlendPose or BlendWeight, change to enum name 
	bool bIsPosePin;
	bool bIsTimePin;
	int32 RawArrayIndex;
	GetPinInformation(Pin->PinName.ToString(), /*out*/ RawArrayIndex, /*out*/ bIsPosePin, /*out*/ bIsTimePin);
	checkSlow(RawArrayIndex == ArrayIndex);
 
	if (bIsPosePin || bIsTimePin)
	{
		if (RawArrayIndex > 0)
		{
			const int32 ExposedEnumPinIndex = RawArrayIndex - 1;
 
			// find pose index and see if it's mapped already or not
			if (ActiveTagEntries.IsValidIndex(ExposedEnumPinIndex))
			{
				const FGameplayTag& ElementTag = ActiveTagEntries[ExposedEnumPinIndex];
				if (ElementTag.IsValid())
				{
					Pin->PinFriendlyName = FText::FromName(ElementTag.GetTagName());
				}
			}
			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);
		}
	}
}
 
 
#undef LOCTEXT_NAMESPACE

[Attachment Removed]

Nice, that looks pretty good. Feel free to make a PR, and I can get it in front of the right people to see if they want to take it. Better to have the option than not. If we did accept it, though, it probably wouldn’t make it into 5.8

Dustin

[Attachment Removed]