You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
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]
418 lines
13 KiB
C++
418 lines
13 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "AnimNodes/AnimNode_RandomPlayer.h"
|
|
|
|
#include "Algo/BinarySearch.h"
|
|
#include "Animation/AnimInstanceProxy.h"
|
|
#include "Animation/AnimSequence.h"
|
|
#include "Animation/AnimTrace.h"
|
|
#include "Animation/AnimSyncScope.h"
|
|
|
|
FAnimNode_RandomPlayer::FAnimNode_RandomPlayer()
|
|
: CurrentPlayDataIndex(0)
|
|
, bShuffleMode(false)
|
|
{
|
|
}
|
|
|
|
void FAnimNode_RandomPlayer::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.
|
|
ValidEntries.Empty(Entries.Num());
|
|
for (int32 EntryIndex = 0; EntryIndex < Entries.Num(); EntryIndex++)
|
|
{
|
|
FRandomPlayerSequenceEntry* Entry = &Entries[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;
|
|
}
|
|
|
|
ValidEntries.Push(Entry);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
NormalizedPlayChances.Empty(NormalizedPlayChances.Num());
|
|
NormalizedPlayChances.AddUninitialized(NumValidEntries + 1);
|
|
|
|
// 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 (FRandomPlayerSequenceEntry* Entry : ValidEntries)
|
|
{
|
|
SumChances += Entry->ChanceToPlay;
|
|
|
|
if (Entry->MaxLoopCount < Entry->MinLoopCount)
|
|
{
|
|
Swap(Entry->MaxLoopCount, Entry->MinLoopCount);
|
|
}
|
|
|
|
if (Entry->MaxPlayRate < Entry->MinPlayRate)
|
|
{
|
|
Swap(Entry->MaxLoopCount, Entry->MinLoopCount);
|
|
}
|
|
|
|
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;
|
|
}
|
|
NormalizedPlayChances[NumValidEntries] = 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.
|
|
FRandomAnimPlayData& CurrentData = GetPlayData(ERandomDataIndexType::Current);
|
|
InitPlayData(CurrentData, CurrentEntry, 1.0f);
|
|
|
|
FRandomAnimPlayData& NextData = GetPlayData(ERandomDataIndexType::Next);
|
|
InitPlayData(NextData, NextEntry, 0.0f);
|
|
}
|
|
|
|
void FAnimNode_RandomPlayer::Update_AnyThread(const FAnimationUpdateContext& Context)
|
|
{
|
|
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;
|
|
}
|
|
|
|
FRandomAnimPlayData* CurrentData = &GetPlayData(ERandomDataIndexType::Current);
|
|
FRandomAnimPlayData* NextData = &GetPlayData(ERandomDataIndexType::Next);
|
|
|
|
const UAnimSequence* 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->PreviousPlayTime;
|
|
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.
|
|
if (AdjustedPreviousPlayTime < CurrentData->PlayStartTime && CurrentData->PlayStartTime <= CurrentData->CurrentPlayTime)
|
|
{
|
|
// 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.
|
|
FRandomPlayerSequenceEntry& 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.
|
|
if (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())
|
|
{
|
|
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->PreviousPlayTime = CurrentData->CurrentPlayTime;
|
|
NextData->PreviousPlayTime = NextData->CurrentPlayTime;
|
|
|
|
if (bAdvanceToNextEntry)
|
|
{
|
|
AdvanceToNextSequence();
|
|
|
|
// Re-get data as we've switched over
|
|
CurrentData = &GetPlayData(ERandomDataIndexType::Current);
|
|
NextData = &GetPlayData(ERandomDataIndexType::Next);
|
|
}
|
|
|
|
FAnimTickRecord TickRecord(CurrentData->Entry->Sequence, true, CurrentData->PlayRate, CurrentData->BlendWeight, CurrentData->CurrentPlayTime, CurrentData->MarkerTickRecord);
|
|
|
|
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, NextData->BlendWeight, NextData->CurrentPlayTime, NextData->MarkerTickRecord);
|
|
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_RandomPlayer::Evaluate_AnyThread(FPoseContext& Output)
|
|
{
|
|
DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(Evaluate_AnyThread)
|
|
if (ValidEntries.Num() == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
FRandomAnimPlayData& CurrentData = GetPlayData(ERandomDataIndexType::Current);
|
|
FRandomAnimPlayData& NextData = GetPlayData(ERandomDataIndexType::Next);
|
|
|
|
UAnimSequence* CurrentSequence = CurrentData.Entry->Sequence;
|
|
|
|
if (!FMath::IsNearlyEqualByULP(CurrentData.BlendWeight, 1.0f))
|
|
{
|
|
FAnimInstanceProxy* AnimProxy = Output.AnimInstanceProxy;
|
|
|
|
// Start Blending
|
|
FCompactPose Poses[2];
|
|
FBlendedCurve Curves[2];
|
|
FStackCustomAttributes 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;
|
|
|
|
UAnimSequence* NextSequence = NextData.Entry->Sequence;
|
|
|
|
|
|
FAnimationPoseData CurrentPoseData(Poses[0], Curves[0], Attributes[0]);
|
|
FAnimationPoseData NextPoseData(Poses[1], Curves[1], Attributes[1]);
|
|
|
|
CurrentSequence->GetAnimationPose(CurrentPoseData, FAnimExtractContext(CurrentData.CurrentPlayTime, AnimProxy->ShouldExtractRootMotion()));
|
|
NextSequence->GetAnimationPose(NextPoseData, FAnimExtractContext(NextData.CurrentPlayTime, AnimProxy->ShouldExtractRootMotion()));
|
|
|
|
FAnimationPoseData AnimationPoseData(Output);
|
|
FAnimationRuntime::BlendPosesTogether(Poses, Curves, Attributes, Weights, AnimationPoseData);
|
|
}
|
|
else
|
|
{
|
|
// Single animation, no blending needed.
|
|
FAnimationPoseData AnimationPoseData(Output);
|
|
CurrentSequence->GetAnimationPose(AnimationPoseData, FAnimExtractContext(CurrentData.CurrentPlayTime, Output.AnimInstanceProxy->ShouldExtractRootMotion()));
|
|
}
|
|
}
|
|
|
|
void FAnimNode_RandomPlayer::GatherDebugData(FNodeDebugData& DebugData)
|
|
{
|
|
DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(GatherDebugData)
|
|
FString DebugLine = DebugData.GetNodeName(this);
|
|
|
|
DebugData.AddDebugItem(DebugLine, true);
|
|
}
|
|
|
|
int32 FAnimNode_RandomPlayer::GetNextValidEntryIndex()
|
|
{
|
|
check(ValidEntries.Num() > 0);
|
|
|
|
if (bShuffleMode)
|
|
{
|
|
// Get the top value, don't allow realloc
|
|
int32 Index = ShuffleList.Pop(false);
|
|
|
|
// 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
|
|
{
|
|
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.
|
|
return Algo::UpperBound(NormalizedPlayChances, RandomVal);
|
|
}
|
|
}
|
|
|
|
FRandomAnimPlayData& FAnimNode_RandomPlayer::GetPlayData(ERandomDataIndexType 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 == ERandomDataIndexType::Current)
|
|
{
|
|
return PlayData[CurrentPlayDataIndex];
|
|
}
|
|
else
|
|
{
|
|
return PlayData[(CurrentPlayDataIndex + 1) % 2];
|
|
}
|
|
}
|
|
|
|
void FAnimNode_RandomPlayer::InitPlayData(FRandomAnimPlayData& Data, int32 ValidEntryIndex, float BlendWeight)
|
|
{
|
|
FRandomPlayerSequenceEntry* Entry = ValidEntries[ValidEntryIndex];
|
|
|
|
Data.Entry = Entry;
|
|
Data.BlendWeight = BlendWeight;
|
|
Data.PlayRate = 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.PreviousPlayTime = 0.0f;
|
|
Data.MarkerTickRecord.Reset();
|
|
}
|
|
|
|
void FAnimNode_RandomPlayer::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
|
|
FRandomAnimPlayData& CurrentData = GetPlayData(ERandomDataIndexType::Current);
|
|
FRandomAnimPlayData& NextData = GetPlayData(ERandomDataIndexType::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_RandomPlayer::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);
|
|
}
|
|
}
|