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;
}
}
}