Random Sequence Player should allow exposing entries by pin

Any the Epic Anim team can tackle this one already?
It’s been a long time coming.
@TomSarkanen @KiaranRitchie @greg.richardson19

1 Like

This will be great for a more modular and efficient character animation variation system. Please don’t forget about this feature.

1 Like

Bumping as well, would love this.

1 Like

was just searching for a way to do this. Pretty useless node if can’t be used for linked layers / instances.

I need that too. To be honest everybody needs that…

Hello ? Is there anyone here ?

+1 for this also

It’s been 7 years

Any new on this post from YEARS ago?
I for one could do with this feature.

+1 this would be soooo helpful and save so much time. Please

I made a new version based on the original version of the engine, you just need to put it in your source code folder like this, then you can search for Random Sequence Player V2 in the animation blueprint.

It has been used in my project, but I still don’t guarantee that it has no bugs. If anyone finds a bug, please let me know.

AnimNode_RandomPlayerSequenceEntry.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "AlphaBlend.h"
#include "Animation/AnimationAsset.h"
#include "CoreMinimal.h"
#include "AnimNode_RandomPlayerSequenceEntry.generated.h"
/** The random player node holds a list of sequences and parameter ranges which will be played continuously
  * In a random order. If shuffle mode is enabled then each entry will be played once before repeating any
  */
USTRUCT(BlueprintType)
struct FRandomPlayerSequenceEntryV2
{
	GENERATED_BODY()

	FRandomPlayerSequenceEntryV2()
	    : Sequence(nullptr)
	    , ChanceToPlay(1.0f)
	    , MinLoopCount(0)
	    , MaxLoopCount(0)
	    , MinPlayRate(1.0f)
	    , MaxPlayRate(1.0f)
	{
	}

	/** Sequence to play when this entry is picked */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Settings", meta = (DisallowedClasses = "/Script/Engine.AnimMontage"))
	TObjectPtr<UAnimSequenceBase> Sequence;

	/** When not in shuffle mode, this is the chance this entry will play (normalized against all other sample chances) */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Settings", meta = (UIMin = "0", ClampMin = "0"))
	float ChanceToPlay;

	/** Minimum number of times this entry will loop before ending */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Settings", meta = (UIMin = "0", ClampMin = "0"))
	int32 MinLoopCount;

	/** Maximum number of times this entry will loop before ending */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Settings", meta = (UIMin = "0", ClampMin = "0"))
	int32 MaxLoopCount;

	/** Minimum playrate for this entry */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Settings", meta = (UIMin = "0", ClampMin = "0"))
	float MinPlayRate;

	/** Maximum playrate for this entry */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Settings", meta = (UIMin = "0", ClampMin = "0"))
	float MaxPlayRate;

	/** Blending properties used when this entry is blending in ontop of another entry */
	UPROPERTY(EditAnywhere, Category = "Settings")
	FAlphaBlend BlendIn;
};

USTRUCT(BlueprintType)
struct FRandomPlayerSequenceEntriesV2
{
	GENERATED_BODY()

	
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Settings")
	TArray<FRandomPlayerSequenceEntryV2> data;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Settings")
	bool repeatable = false;
};

AnimGraphNode_RandomPlayer2.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "CoreMinimal.h"
#include "UObject/ObjectMacros.h"
#include "AnimGraphNode_Base.h"
#include "AnimNode_RandomPlayer2.h"
#include "AnimGraphNode_RandomPlayer2.generated.h"

UCLASS(MinimalAPI)
class UAnimGraphNode_RandomPlayer2 : public UAnimGraphNode_Base
{
	GENERATED_BODY()

public:

	UPROPERTY(EditAnywhere, Category=Settings)
	FAnimNode_RandomPlayerV2 Node;

	// UEdGraphNode interface
	virtual FLinearColor GetNodeTitleColor() const override;
	virtual FText GetTooltipText() const override;
	virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override;
	virtual FText GetMenuCategory() const override;
	// End of UEdGraphNode interface

	// UAnimGraphNode_Base interface
	virtual void GetOutputLinkAttributes(FNodeAttributeArray& OutAttributes) const override;
	virtual void PreloadRequiredAssets() override;
	// End of UAnimGraphNode_Base interface
};

AnimGraphNode_RandomPlayer2.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "AnimGraphNode_RandomPlayer2.h"

#include "EditorCategoryUtils.h"
#include "Animation/AnimAttributes.h"
#include "Animation/AnimRootMotionProvider.h"

#define LOCTEXT_NAMESPACE "AnimGraphNode_RandomPlayer"

FLinearColor UAnimGraphNode_RandomPlayer2::GetNodeTitleColor() const
{
	return FLinearColor(0.10f, 0.60f, 0.12f);
}

FText UAnimGraphNode_RandomPlayer2::GetTooltipText() const
{
	return LOCTEXT("NodeToolTip", "Plays sequences picked from a provided list in random orders.");
}

FText UAnimGraphNode_RandomPlayer2::GetNodeTitle(ENodeTitleType::Type TitleType) const
{
	return LOCTEXT("NodeTitle", "Random Sequence Player V2");
}

FText UAnimGraphNode_RandomPlayer2::GetMenuCategory() const
{
	return LOCTEXT("NodeCategory", "Animation|Sequences");
}

void UAnimGraphNode_RandomPlayer2::GetOutputLinkAttributes(FNodeAttributeArray& OutAttributes) const
{
	OutAttributes.Add(UE::Anim::FAttributes::Curves);
	OutAttributes.Add(UE::Anim::FAttributes::Attributes);

	if (UE::Anim::IAnimRootMotionProvider::Get())
	{
		OutAttributes.Add(UE::Anim::IAnimRootMotionProvider::AttributeName);
	}
}

void UAnimGraphNode_RandomPlayer2::PreloadRequiredAssets()
{
	for (const FRandomPlayerSequenceEntryV2& Entry : Node.Entries.data)
	{
		PreloadObject(Entry.Sequence);
	}

	Super::PreloadRequiredAssets();
}

#undef LOCTEXT_NAMESPACE

AnimNode_RandomPlayer2.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "AlphaBlend.h"
#include "Animation/AnimNode_RelevantAssetPlayerBase.h"
#include "Animation/AnimationAsset.h"
#include "CoreMinimal.h"
#include "Math/RandomStream.h"
#include "UObject/ObjectMacros.h"
#include "AnimNode_RandomPlayerSequenceEntry.h"
#include <mutex>
#include "AnimNode_RandomPlayer2.generated.h"

enum class ERandomDataIndexTypeV2
{
	Current,
	Next,
};

struct FRandomAnimPlayDataV2
{
	// Index into the real sequence entry list, not the valid entry list.
	FRandomPlayerSequenceEntryV2* Entry = nullptr;

	// The time at which the animation started playing. Used to initialize
	// the play for this animation and detect when a loop has occurred.
	float PlayStartTime = 0.0f;

	// The time at which the animation is currently playing.
	float CurrentPlayTime = 0.0f;

	// Delta time record for this play through
	FDeltaTimeRecord DeltaTimeRecord;

	// Calculated play rate
	float PlayRate = 0.0f;

	// Current blend weight
	float BlendWeight = 0.0f;

	// Calculated loops remaining
	int32 RemainingLoops = 0;

	// Marker tick record for this play through
	FMarkerTickRecord MarkerTickRecord;
};

USTRUCT(BlueprintInternalUseOnly)
struct FAnimNode_RandomPlayerV2 : public FAnimNode_AssetPlayerRelevancyBase
{
	GENERATED_BODY()

	CRISISINKARNO_API FAnimNode_RandomPlayerV2();
	virtual ~FAnimNode_RandomPlayerV2();

public:
	/** List of sequences to randomly step through */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Settings", meta = (PinHiddenByDefault))
	FRandomPlayerSequenceEntriesV2 Entries;

	// FAnimNode_Base interface
	CRISISINKARNO_API virtual void Initialize_AnyThread(const FAnimationInitializeContext& Context) override;
	CRISISINKARNO_API virtual void Update_AnyThread(const FAnimationUpdateContext& Context) override;
	CRISISINKARNO_API virtual void Evaluate_AnyThread(FPoseContext& Output) override;
	CRISISINKARNO_API virtual void GatherDebugData(FNodeDebugData& DebugData) override;
	// End of FAnimNode_Base interface

	// FAnimNode_RelevantAssetPlayerBase
	CRISISINKARNO_API virtual UAnimationAsset* GetAnimAsset() const override;
	CRISISINKARNO_API virtual float GetAccumulatedTime() const override;
	CRISISINKARNO_API virtual bool GetIgnoreForRelevancyTest() const override;
	CRISISINKARNO_API virtual bool SetIgnoreForRelevancyTest(bool bInIgnoreForRelevancyTest) override;
	CRISISINKARNO_API virtual float GetCachedBlendWeight() const override;
	CRISISINKARNO_API virtual void ClearCachedBlendWeight() override;
	CRISISINKARNO_API virtual const FDeltaTimeRecord* GetDeltaTimeRecord() const override;
	// End of FAnimNode_RelevantAssetPlayerBase

private:
	// Return the index of the next FRandomPlayerSequenceEntryV2 to play, from the list
	// of valid playable entries (ValidEntries).
	int32 GetNextValidEntryIndex();

	// Return the play data for either the currently playing animation or the next
	// animation to blend into.
	FRandomAnimPlayDataV2& GetPlayData(ERandomDataIndexTypeV2 Type);
	const FRandomAnimPlayDataV2& GetPlayData(ERandomDataIndexTypeV2 Type) const;

	// Initialize the play data with the given index into the ValidEntries array and
	// a specific blend weight. All other member data will be reset to their default values.
	void InitPlayData(FRandomAnimPlayDataV2& Data, int32 InValidEntryIndex, float InBlendWeight);

	// Advance to the next playable sequence. This is only called once a sequence is fully
	// blended or there's a hard switch to the same playable entry.
	void AdvanceToNextSequence();

	// Build a new ShuffleList array, which is a shuffled index list of all the valid
	// playable entries in ValidEntries. The LastEntry can be set to a valid entry index to
	// ensure that the top/last item in the shuffle list will be a different value from it;
	// pass in INDEX_NONE to disable the check.
	void BuildShuffleList(int32 LastEntry);

	void freeValidEntries();

	// List of valid sequence entries
	TArray<FRandomPlayerSequenceEntryV2*> ValidEntries;

	// Normalized list of play chances when we aren't using shuffle mode
	TArray<float> NormalizedPlayChances;

	// Play data for the current and next sequence
	TArray<FRandomAnimPlayDataV2> PlayData;

	// Index of the 'current' data set in the PlayData array.
	int32 CurrentPlayDataIndex;

	// List to store transient shuffle stack in shuffle mode.
	TArray<int32> ShuffleList;

	// Random number source
	FRandomStream RandomStream;

#if WITH_EDITORONLY_DATA
	// If true, "Relevant anim" nodes that look for the highest weighted animation in a state will ignore this node
	UPROPERTY(EditAnywhere, Category = Relevancy, meta = (FoldProperty, PinHiddenByDefault))
	bool bIgnoreForRelevancyTest = false;
#endif // WITH_EDITORONLY_DATA

protected:
	/** Last encountered blend weight for this node */
	UPROPERTY(BlueprintReadWrite, Transient, Category = DoNotEdit)
	float BlendWeight = 0.0f;

	bool repeatable = false;
	int lastRandomIndex = -1;

public:
	/** When shuffle mode is active we will never loop a sequence beyond MaxLoopCount
	  * without visiting each sequence in turn (no repeats). Enabling this will ignore
	  * ChanceToPlay for each entry
	  */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Settings")
	bool bShuffleMode;
};

AnimNode_RandomPlayer2.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "AnimNode_RandomPlayer2.h"

#include "Algo/BinarySearch.h"
#include "Animation/AnimInstanceProxy.h"
#include "Animation/AnimSequence.h"
#include "Animation/AnimTrace.h"
#include "Animation/AnimStats.h"
#include "Animation/AnimSyncScope.h"

#include UE_INLINE_GENERATED_CPP_BY_NAME(AnimNode_RandomPlayer2)

FAnimNode_RandomPlayerV2::FAnimNode_RandomPlayerV2()
    : CurrentPlayDataIndex(0)
    , bShuffleMode(false)
{
}

FAnimNode_RandomPlayerV2::~FAnimNode_RandomPlayerV2()
{
	freeValidEntries();
}


void FAnimNode_RandomPlayerV2::Initialize_AnyThread(const FAnimationInitializeContext& Context)
{
	DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(Initialize_AnyThread)
	FAnimNode_Base::Initialize_AnyThread(Context);
	GetEvaluateGraphExposedInputs().Execute(Context);

	// Create a sanitized list of valid entries and only operate on those from here on in.
	freeValidEntries();
	ValidEntries.Empty(Entries.data.Num());
	for (int32 EntryIndex = 0; EntryIndex < Entries.data.Num(); EntryIndex++)
	{
		FRandomPlayerSequenceEntryV2* Entry = &Entries.data[EntryIndex];

		if (Entry->Sequence == nullptr)
		{
			continue;
		}

		// If the likelihood of this entry playing is nil, then skip it as well.
		if (!bShuffleMode && Entry->ChanceToPlay <= SMALL_NUMBER)
		{
			continue;
		}

		FRandomPlayerSequenceEntryV2* newEntry = new FRandomPlayerSequenceEntryV2(*Entry);
		ValidEntries.Push(newEntry);
	}

	const int32 NumValidEntries = ValidEntries.Num();

	if (NumValidEntries == 0)
	{
		// early out here, no need to do anything at all if we're not playing anything
		return;
	}
	repeatable = Entries.repeatable;
	lastRandomIndex = -1;

	NormalizedPlayChances.Empty(NormalizedPlayChances.Num());
	NormalizedPlayChances.AddUninitialized(NumValidEntries);

	// Sanitize the data and sum up the range of the random chances so that
	// we can normalize the individual chances below.
	float SumChances = 0.0f;
	for (FRandomPlayerSequenceEntryV2* Entry : ValidEntries)
	{
		SumChances += Entry->ChanceToPlay;

		if (Entry->MaxLoopCount < Entry->MinLoopCount)
		{
			Swap(Entry->MaxLoopCount, Entry->MinLoopCount);
		}

		if (Entry->MaxPlayRate < Entry->MinPlayRate)
		{
			Swap(Entry->MaxPlayRate, Entry->MinPlayRate);
		}

		Entry->BlendIn.Reset();
	}

	if (bShuffleMode)
	{
		// Seed the shuffle list, ignoring all last entry checks, since we're doing the
		// initial build and don't care about the non-repeatability property (yet).
		BuildShuffleList(INDEX_NONE);
	}
	else
	{
		// Ensure that our chance sum is non-"zero" and non-negative.
		check(SumChances > SMALL_NUMBER);

		// Construct a cumulative distribution function so that we can look up the
		// index of the sequence using binary search on the [0-1) random number.
		float CurrentChance = 0.0f;
		for (int32 Idx = 0; Idx < NumValidEntries; ++Idx)
		{
			CurrentChance += ValidEntries[Idx]->ChanceToPlay / SumChances;
			NormalizedPlayChances[Idx] = CurrentChance;
		}
		// Remove rounding errors (possibly slightly padding out the chance of the last item)
		NormalizedPlayChances[NumValidEntries - 1] = 1.0f;
	}

	// Initialize random stream and pick first entry
	RandomStream.Initialize(FPlatformTime::Cycles());

	PlayData.Empty(2);
	PlayData.AddDefaulted(2);

	int32 CurrentEntry = GetNextValidEntryIndex();
	int32 NextEntry = GetNextValidEntryIndex();

	// Initialize the animation data for the first and the next sequence so that we can properly
	// blend between them.
	FRandomAnimPlayDataV2& CurrentData = GetPlayData(ERandomDataIndexTypeV2::Current);
	InitPlayData(CurrentData, CurrentEntry, 1.0f);

	FRandomAnimPlayDataV2& NextData = GetPlayData(ERandomDataIndexTypeV2::Next);
	InitPlayData(NextData, NextEntry, 0.0f);
}

void FAnimNode_RandomPlayerV2::Update_AnyThread(const FAnimationUpdateContext& Context)
{
	BlendWeight = Context.GetFinalBlendWeight();

	DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(Update_AnyThread)
	GetEvaluateGraphExposedInputs().Execute(Context);

	if (ValidEntries.Num() == 0)
	{
		// We don't have any entries, play data will be invalid - early out
		return;
	}

	FRandomAnimPlayDataV2* CurrentData = &GetPlayData(ERandomDataIndexTypeV2::Current);
	FRandomAnimPlayDataV2* NextData = &GetPlayData(ERandomDataIndexTypeV2::Next);

	const UAnimSequenceBase* CurrentSequence = CurrentData->Entry->Sequence;
	

	// If we looped around, adjust the previous play time to always be before the current playtime,
	// since we can assume modulo. This makes the crossing check for the start time a lot simpler.
	float AdjustedPreviousPlayTime = CurrentData->DeltaTimeRecord.GetPrevious();
	if (CurrentData->CurrentPlayTime < AdjustedPreviousPlayTime)
	{
		AdjustedPreviousPlayTime -= CurrentSequence->GetPlayLength();
	}

	// Did we cross the play start time? Decrement the loop counter. Once we're on the last loop, we can
	// start blending into the next animation.
	bool bHasLooped = AdjustedPreviousPlayTime < CurrentData->PlayStartTime && CurrentData->PlayStartTime <= CurrentData->CurrentPlayTime;
	if (bHasLooped)
	{
		// We've looped, update remaining
		--CurrentData->RemainingLoops;
	}

	bool bAdvanceToNextEntry = false;

	if (CurrentData->RemainingLoops <= 0)
	{
		const bool bNextAnimIsDifferent = CurrentData->Entry != NextData->Entry;

		// If we're in the blend window start blending, but only if we're moving to a new animation,
		// otherwise just keep looping.
		FRandomPlayerSequenceEntryV2& NextSequenceEntry = *NextData->Entry;

		// If the next animation is different, then smoothly blend between them. Otherwise
		// we do a hard transition to the same play point. The next animation might play at
		// a different rate, so we have to switch.
		if (bNextAnimIsDifferent)
		{
			bool bDoBlending = false;

			// Are we already blending? Continue to do so. Special case for zero blend time as alpha will always be 1.
			if (NextSequenceEntry.BlendIn.GetBlendTime() > 0.0f && FAnimationRuntime::HasWeight(NextSequenceEntry.BlendIn.GetAlpha()))
			{
				bDoBlending = true;
			}
			else
			{
				// Check to see if we need to start the blending process.
				float AmountPlayedSoFar = CurrentData->CurrentPlayTime - CurrentData->PlayStartTime;
				if (AmountPlayedSoFar < 0.0f)
				{
					AmountPlayedSoFar += CurrentSequence->GetPlayLength();
				}

				float TimeRemaining = CurrentSequence->GetPlayLength() - AmountPlayedSoFar;

				if (TimeRemaining <= NextSequenceEntry.BlendIn.GetBlendTime() || bHasLooped)
				{
					bDoBlending = true;
				}
			}

			if (bDoBlending)
			{
				// Blending to next
				NextSequenceEntry.BlendIn.Update(Context.GetDeltaTime());

				if (NextSequenceEntry.BlendIn.IsComplete())
				{
					// Set the play start time to be the current play time so that loop counts are properly
					// maintained.
					NextData->PlayStartTime = NextData->CurrentPlayTime;
					bAdvanceToNextEntry = true;
				}
				else
				{
					 float BlendedAlpha = NextSequenceEntry.BlendIn.GetBlendedValue();

					if (BlendedAlpha < 1.0f)
					{
						NextData->BlendWeight = BlendedAlpha;
						CurrentData->BlendWeight = 1.0f - BlendedAlpha;
					}
				}
			}
		}
		else if (!bNextAnimIsDifferent && CurrentData->RemainingLoops < 0)
		{
			NextData->CurrentPlayTime = CurrentData->CurrentPlayTime;

			// Set the play start time to be the current play time so that loop counts are properly
			// maintained.
			NextData->PlayStartTime = NextData->CurrentPlayTime;
			bAdvanceToNextEntry = true;
		}
	}

	// Cache time to detect loops
	CurrentData->DeltaTimeRecord.SetPrevious(CurrentData->CurrentPlayTime);
	NextData->DeltaTimeRecord.SetPrevious(NextData->CurrentPlayTime);

	if (bAdvanceToNextEntry)
	{
		AdvanceToNextSequence();

		// Re-get data as we've switched over
		CurrentData = &GetPlayData(ERandomDataIndexTypeV2::Current);
		NextData = &GetPlayData(ERandomDataIndexTypeV2::Next);
	}

	FAnimTickRecord TickRecord(CurrentData->Entry->Sequence, true, CurrentData->PlayRate, false, CurrentData->BlendWeight, CurrentData->CurrentPlayTime, CurrentData->MarkerTickRecord);
	TickRecord.DeltaTimeRecord = &CurrentData->DeltaTimeRecord;
	TickRecord.GatherContextData(Context);

	UE::Anim::FAnimSyncGroupScope& SyncScope = Context.GetMessageChecked<UE::Anim::FAnimSyncGroupScope>();
	SyncScope.AddTickRecord(TickRecord, UE::Anim::FAnimSyncParams(), UE::Anim::FAnimSyncDebugInfo(Context));

	TRACE_ANIM_TICK_RECORD(Context, TickRecord);

	if (FAnimationRuntime::HasWeight(NextData->BlendWeight))
	{
		FAnimTickRecord NextTickRecord(NextData->Entry->Sequence, true, NextData->PlayRate, false, NextData->BlendWeight, NextData->CurrentPlayTime, NextData->MarkerTickRecord);
		NextTickRecord.DeltaTimeRecord = &NextData->DeltaTimeRecord;
		NextTickRecord.GatherContextData(Context);

		SyncScope.AddTickRecord(NextTickRecord, UE::Anim::FAnimSyncParams(), UE::Anim::FAnimSyncDebugInfo(Context));

		TRACE_ANIM_TICK_RECORD(Context, NextTickRecord);
	}

	TRACE_ANIM_NODE_VALUE(Context, TEXT("Current Sequence"), CurrentData ? CurrentData->Entry->Sequence : nullptr);
	TRACE_ANIM_NODE_VALUE(Context, TEXT("Current Weight"), CurrentData ? CurrentData->BlendWeight : 0.0f);
	TRACE_ANIM_NODE_VALUE(Context, TEXT("Next Sequence"), NextData ? NextData->Entry->Sequence : nullptr);
	TRACE_ANIM_NODE_VALUE(Context, TEXT("Next Weight"), NextData ? NextData->BlendWeight : 0.0f);
}

void FAnimNode_RandomPlayerV2::Evaluate_AnyThread(FPoseContext& Output)
{
	DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(Evaluate_AnyThread)
	ANIM_MT_SCOPE_CYCLE_COUNTER_VERBOSE(RandomPlayer, !IsInGameThread());

	if (ValidEntries.Num() == 0)
	{
		Output.ResetToRefPose();
		return;
	}

	FRandomAnimPlayDataV2& CurrentData = GetPlayData(ERandomDataIndexTypeV2::Current);
	FRandomAnimPlayDataV2& NextData = GetPlayData(ERandomDataIndexTypeV2::Next);

	UAnimSequenceBase* CurrentSequence = CurrentData.Entry->Sequence;

	if (!FMath::IsNearlyEqualByULP(CurrentData.BlendWeight, 1.0f))
	{
		FAnimInstanceProxy* AnimProxy = Output.AnimInstanceProxy;

		// Start Blending
		FCompactPose Poses[2];
		FBlendedCurve Curves[2];
		UE::Anim::FStackAttributeContainer Attributes[2];
		float Weights[2];

		const FBoneContainer& RequiredBone = AnimProxy->GetRequiredBones();
		Poses[0].SetBoneContainer(&RequiredBone);
		Poses[1].SetBoneContainer(&RequiredBone);

		Curves[0].InitFrom(RequiredBone);
		Curves[1].InitFrom(RequiredBone);

		Weights[0] = CurrentData.BlendWeight;
		Weights[1] = NextData.BlendWeight;

		UAnimSequenceBase* NextSequence = NextData.Entry->Sequence;

		FAnimationPoseData CurrentPoseData(Poses[0], Curves[0], Attributes[0]);
		FAnimationPoseData NextPoseData(Poses[1], Curves[1], Attributes[1]);

		CurrentSequence->GetAnimationPose(CurrentPoseData, FAnimExtractContext(static_cast<double>(CurrentData.CurrentPlayTime), AnimProxy->ShouldExtractRootMotion(), CurrentData.DeltaTimeRecord, CurrentData.RemainingLoops > 0));
		NextSequence->GetAnimationPose(NextPoseData, FAnimExtractContext(static_cast<double>(NextData.CurrentPlayTime), AnimProxy->ShouldExtractRootMotion(), NextData.DeltaTimeRecord, NextData.RemainingLoops > 0));

		FAnimationPoseData AnimationPoseData(Output);
		FAnimationRuntime::BlendPosesTogether(Poses, Curves, Attributes, Weights, AnimationPoseData);
	}
	else
	{
		// Single animation, no blending needed.
		FAnimationPoseData AnimationPoseData(Output);
		CurrentSequence->GetAnimationPose(AnimationPoseData, FAnimExtractContext(static_cast<double>(CurrentData.CurrentPlayTime), Output.AnimInstanceProxy->ShouldExtractRootMotion(), CurrentData.DeltaTimeRecord, CurrentData.RemainingLoops > 0));
	}
}

void FAnimNode_RandomPlayerV2::GatherDebugData(FNodeDebugData& DebugData)
{
	DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(GatherDebugData)
	FString DebugLine = DebugData.GetNodeName(this);

	DebugData.AddDebugItem(DebugLine, true);
}

UAnimationAsset* FAnimNode_RandomPlayerV2::GetAnimAsset() const
{
	UAnimationAsset* AnimationAsset = nullptr;

	if (ValidEntries.Num() > 0)
	{
		const FRandomAnimPlayDataV2& CurrentPlayData = GetPlayData(ERandomDataIndexTypeV2::Current);
		AnimationAsset = (CurrentPlayData.Entry != nullptr) ? CurrentPlayData.Entry->Sequence : nullptr;
	}

	return AnimationAsset;
}

float FAnimNode_RandomPlayerV2::GetAccumulatedTime() const
{
	float AccumulatedTime = 0.f;

	if (ValidEntries.Num() > 0)
	{
		const FRandomAnimPlayDataV2& CurrentPlayData = GetPlayData(ERandomDataIndexTypeV2::Current);

		return CurrentPlayData.CurrentPlayTime;
	}

	return AccumulatedTime;
}

bool FAnimNode_RandomPlayerV2::GetIgnoreForRelevancyTest() const
{
	return GET_ANIM_NODE_DATA(bool, bIgnoreForRelevancyTest);
}

bool FAnimNode_RandomPlayerV2::SetIgnoreForRelevancyTest(bool bInIgnoreForRelevancyTest)
{
#if WITH_EDITORONLY_DATA
	bIgnoreForRelevancyTest = bInIgnoreForRelevancyTest;
#endif

	if (bool* bIgnoreForRelevancyTestPtr = GET_INSTANCE_ANIM_NODE_DATA_PTR(bool, bIgnoreForRelevancyTest))
	{
		*bIgnoreForRelevancyTestPtr = bInIgnoreForRelevancyTest;
		return true;
	}

	return false;
}

float FAnimNode_RandomPlayerV2::GetCachedBlendWeight() const
{
	return BlendWeight;
}

void FAnimNode_RandomPlayerV2::ClearCachedBlendWeight()
{
	BlendWeight = 0.f;
}

const FDeltaTimeRecord* FAnimNode_RandomPlayerV2::GetDeltaTimeRecord() const
{
	if (ValidEntries.Num() > 0)
	{
		const FRandomAnimPlayDataV2& CurrentPlayData = GetPlayData(ERandomDataIndexTypeV2::Current);
		return &CurrentPlayData.DeltaTimeRecord;
	}
	return nullptr;
}

int32 FAnimNode_RandomPlayerV2::GetNextValidEntryIndex()
{
	check(ValidEntries.Num() > 0);

	if (bShuffleMode)
	{
		// Get the top value, don't allow realloc
		int32 Index = ShuffleList.Pop(EAllowShrinking::No);

		// If we cleared the shuffles, rebuild for the next round, indicating
		// the current value so that we don't pop that one off again next time.
		if (ShuffleList.Num() == 0)
		{
			BuildShuffleList(Index);
		}

		return Index;
	}
	else
	{
		if (ValidEntries.Num() == 1)
			return 0;

		while (true)
		{
			float RandomVal = RandomStream.GetFraction();

			// Search the cumulative distribution array for the last entry that's
			// smaller or equal to the random value. That becomes our new animation.
			int index = Algo::UpperBound(NormalizedPlayChances, RandomVal);
			if (repeatable)
				return index;

			if (index != lastRandomIndex)
			{
				lastRandomIndex = index;
				return index;
			}
		}
	}
}

FRandomAnimPlayDataV2& FAnimNode_RandomPlayerV2::GetPlayData(ERandomDataIndexTypeV2 Type)
{
	// PlayData only holds two entries. We swap between them in AdvanceToNextSequence
	// by setting CUrrentPlayDataIndex to either 0 or 1. Hence the modulo 2 magic below.
	if (Type == ERandomDataIndexTypeV2::Current)
	{
		return PlayData[CurrentPlayDataIndex];
	}
	else
	{
		return PlayData[(CurrentPlayDataIndex + 1) % 2];
	}
}

const FRandomAnimPlayDataV2& FAnimNode_RandomPlayerV2::GetPlayData(ERandomDataIndexTypeV2 Type) const
{
	return const_cast<FAnimNode_RandomPlayerV2*>(this)->GetPlayData(Type);
}

void FAnimNode_RandomPlayerV2::InitPlayData(FRandomAnimPlayDataV2& Data, int32 InValidEntryIndex, float InBlendWeight)
{
	FRandomPlayerSequenceEntryV2* Entry = ValidEntries[InValidEntryIndex];

	Data.Entry = Entry;
	Data.BlendWeight = InBlendWeight;
	Data.PlayRate = static_cast<float>(RandomStream.FRandRange(Entry->MinPlayRate, Entry->MaxPlayRate));
	Data.RemainingLoops = FMath::Clamp(RandomStream.RandRange(Entry->MinLoopCount, Entry->MaxLoopCount), 0, MAX_int32);

	Data.PlayStartTime = 0.0f;
	Data.CurrentPlayTime = 0.0f;
	Data.DeltaTimeRecord = FDeltaTimeRecord();
	Data.MarkerTickRecord.Reset();
}

void FAnimNode_RandomPlayerV2::AdvanceToNextSequence()
{
	// Get the next sequence entry to use.
	int32 NextEntry = GetNextValidEntryIndex();

	// Switch play data by flipping it between 0 and 1.
	CurrentPlayDataIndex = (CurrentPlayDataIndex + 1) % 2;

	// Get our play data
	FRandomAnimPlayDataV2& CurrentData = GetPlayData(ERandomDataIndexTypeV2::Current);
	FRandomAnimPlayDataV2& NextData = GetPlayData(ERandomDataIndexTypeV2::Next);

	// Reset blend weights
	CurrentData.BlendWeight = 1.0f;
	CurrentData.Entry->BlendIn.Reset();

	// Set up data for next switch
	InitPlayData(NextData, NextEntry, 0.0f);
}

void FAnimNode_RandomPlayerV2::BuildShuffleList(int32 LastEntry)
{
	ShuffleList.Reset(ValidEntries.Num());

	// Build entry index list
	const int32 NumValidEntries = ValidEntries.Num();
	for (int32 i = 0; i < NumValidEntries; ++i)
	{
		ShuffleList.Add(i);
	}

	// Shuffle the list
	const int32 NumShuffles = ShuffleList.Num() - 1;
	for (int32 i = 0; i < NumShuffles; ++i)
	{
		int32 SwapIdx = RandomStream.RandRange(i, NumShuffles);
		ShuffleList.Swap(i, SwapIdx);
	}

	// Make sure we don't play the same thing twice in a row
	if (ShuffleList.Num() > 1 && ShuffleList.Last() == LastEntry)
	{
		// Swap the last with a random entry.
		ShuffleList.Swap(RandomStream.RandRange(0, ShuffleList.Num() - 2), ShuffleList.Num() - 1);
	}
}

void FAnimNode_RandomPlayerV2::freeValidEntries()
{
	for (auto it : ValidEntries)
	{
		delete it;
	}

	ValidEntries.Empty();
}