Files
UnrealEngine/Engine/Plugins/Animation/PoseSearch/Source/Runtime/Private/PoseSearchInteractionSubsystem.cpp
2025-05-18 13:04:45 +08:00

1325 lines
52 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "PoseSearch/PoseSearchInteractionSubsystem.h"
#include "Components/PrimitiveComponent.h"
#include "Engine/World.h"
#include "PoseSearch/AnimNode_PoseSearchHistoryCollector.h"
#include "PoseSearch/PoseSearchDatabase.h"
#include "PoseSearch/PoseSearchInteractionIsland.h"
#include "PoseSearch/PoseSearchInteractionUtils.h"
#include "PoseSearch/PoseSearchRole.h"
#include "PoseSearch/PoseSearchSchema.h"
#include "VisualLogger/VisualLogger.h"
namespace UE::PoseSearch
{
#if !NO_CVARS
static bool GVarPoseSearchInteractionEnabled = true;
static FAutoConsoleVariableRef CVarPoseSearchInteractionEnabled(TEXT("a.PoseSearchInteraction.Enabled"), GVarPoseSearchInteractionEnabled, TEXT("Enable/Disable Pose Search Interaction"));
static bool GVarPoseSearchInteractionCacheIslands = true;
static FAutoConsoleVariableRef CVarPoseSearchInteractionCacheIslands(TEXT("a.PoseSearchInteraction.CacheIslands"), GVarPoseSearchInteractionCacheIslands, TEXT("Cache Pose Search Interaction Islands for future reuse instead of destrying them"));
static bool GVarPoseSearchInteractionLoglandsTickDependencies = false;
static FAutoConsoleVariableRef CVarPoseSearchInteractionLoglandsTickDependencies(TEXT("a.PoseSearchInteraction.LoglandsTickDependencies"), GVarPoseSearchInteractionLoglandsTickDependencies, TEXT("Log islands tick dependencies"));
#endif // !NO_CVARS
struct FAnimContextInfo
{
void Init(const FPoseSearchInteractionAnimContextAvailabilities& InAnimContextAvailabilities)
{
check(InAnimContextAvailabilities.AnimContext && !InAnimContextAvailabilities.Availabilities.IsEmpty());
AnimContextAvailabilities = &InAnimContextAvailabilities;
Location = GetContextLocation(InAnimContextAvailabilities.AnimContext);
}
// performs broad phase analysis checking if at least one of the Availabilities associated to AnimContext can interact with OtherAnimContextInfo.
// This is a more relaxed analysis than the one performed in FRoledAnimContextInfo::CanInteractWith
bool CanInteractWith(const FAnimContextInfo& OtherAnimContextInfo) const
{
check(this != &OtherAnimContextInfo);
// @todo: enable this code if we ended up requiring preventing interactions between the same actor!
//check(AnimContextAvailabilities && OtherAnimContextInfo.AnimContextAvailabilities);
//const AActor* AnimContextActor = GetContextOwningActor(AnimContextAvailabilities->AnimContext);
//const AActor* OtherAnimContextActor = GetContextOwningActor(OtherAnimContextInfo.AnimContextAvailabilities->AnimContext);
//if (AnimContextActor == OtherAnimContextActor)
//{
// return false;
//}
const FVector DeltaLocation = Location - OtherAnimContextInfo.Location;
const float DistanceSquared = DeltaLocation.SquaredLength();
const float MaxDistance = FMath::Min(GetAvailabilitiesMaxBroadPhaseRadius(), OtherAnimContextInfo.GetAvailabilitiesMaxBroadPhaseRadius());
const float MaxDistanceSquared = MaxDistance * MaxDistance;
return DistanceSquared <= MaxDistanceSquared;
}
float GetAvailabilitiesMaxBroadPhaseRadius() const
{
float AvailabilitiesMaxBroadPhaseRadius = 0.f;
check(AnimContextAvailabilities);
for (const FPoseSearchInteractionAvailabilityEx& Availability : AnimContextAvailabilities->Availabilities)
{
// @todo: optimize the AvailabilitiesMaxBroadPhaseRadius, since adding Availability.BroadPhaseRadiusIncrementOnInteraction is required ONLY if AnimContext is already part of an interaction
AvailabilitiesMaxBroadPhaseRadius = FMath::Max(AvailabilitiesMaxBroadPhaseRadius, Availability.BroadPhaseRadius + Availability.BroadPhaseRadiusIncrementOnInteraction);
}
return AvailabilitiesMaxBroadPhaseRadius;
}
const FPoseSearchInteractionAnimContextAvailabilities* AnimContextAvailabilities = nullptr;
// cached AnimContext location
FVector Location = FVector::ZeroVector;
TArray<const FAnimContextInfo*, TInlineAllocator<8, TMemStackAllocator<>>> NearbyAnimContextInfos;
};
struct FAnimContextInfos : TArray<FAnimContextInfo, TMemStackAllocator<>> {};
typedef TArray<const UPoseSearchDatabase*, TInlineAllocator<32, TMemStackAllocator<>>> FDatabasesPerTag;
struct FTagToDatabases : TMap<FName, FDatabasesPerTag, TInlineSetAllocator<8, TMemStackSetAllocator<>>> {};
} // namespace UE::PoseSearch
// FPoseSearchInteractionAvailabilityEx
///////////////////////////////////////////////
FString FPoseSearchInteractionAvailabilityEx::GetPoseHistoryName() const
{
if (PoseHistory)
{
return "HistoryProvider";
}
return PoseHistoryName.ToString();
}
const UE::PoseSearch::IPoseHistory* FPoseSearchInteractionAvailabilityEx::GetPoseHistory(const UObject* AnimContext) const
{
if (PoseHistory)
{
return PoseHistory;
}
if (const UAnimInstance* AnimInstance = Cast<UAnimInstance>(AnimContext))
{
if (const FAnimNode_PoseSearchHistoryCollector_Base* PoseSearchHistoryCollector = UPoseSearchLibrary::FindPoseHistoryNode(PoseHistoryName, AnimInstance))
{
return &PoseSearchHistoryCollector->GetPoseHistory();
}
}
unimplemented();
return nullptr;
}
// UPoseSearchInteractionSubsystem
///////////////////////////////////////////////
UE::PoseSearch::FInteractionIsland& UPoseSearchInteractionSubsystem::CreateIsland()
{
return *Islands.Add_GetRef(new UE::PoseSearch::FInteractionIsland(ToRawPtr(GetWorld()->PersistentLevel), this));
}
void UPoseSearchInteractionSubsystem::DestroyIsland(int32 Index)
{
delete Islands[Index];
Islands.RemoveAt(Index);
}
UE::PoseSearch::FInteractionIsland& UPoseSearchInteractionSubsystem::GetAvailableIsland()
{
using namespace UE::PoseSearch;
for (FInteractionIsland* Island : Islands)
{
if (!Island->IsInitialized())
{
return *Island;
}
}
return CreateIsland();
}
void UPoseSearchInteractionSubsystem::DestroyAllIslands()
{
for (int32 IslandIndex = Islands.Num() - 1; IslandIndex >= 0; --IslandIndex)
{
DestroyIsland(IslandIndex);
}
}
void UPoseSearchInteractionSubsystem::RegenerateAllIslands(float DeltaSeconds)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_UPoseSearchInteractionSubsystem_RegenerateAllIslands);
using namespace UE::PoseSearch;
check(IsInGameThread());
// FScopeLock Lock(&AnimContextsAvailabilitiesMutex); is not necessary since UPoseSearchInteractionSubsystem gets ticked outside the parallel animation jobs
// generating all the possible interaction tuples of AnimContext(s) with roles and pose histories (defined in FInteractionSearchContext)
FInteractionSearchContexts SearchContexts;
GenerateSearchContexts(DeltaSeconds, SearchContexts);
#if ENABLE_DRAW_DEBUG && ENABLE_ANIM_DEBUG
// drawing the current frame islands to be consistent with the search, before regenerating the islands with the newly published availabilities
DebugDrawIslands();
#endif // ENABLE_DRAW_DEBUG && ENABLE_ANIM_DEBUG
#if ENABLE_ANIM_DEBUG
DebugLogTickDependencies();
#endif // ENABLE_ANIM_DEBUG
#if !NO_CVARS
if (!GVarPoseSearchInteractionCacheIslands)
{
// not caching the islands. Destroy them all!
DestroyAllIslands();
}
else
#endif // !NO_CVARS
{
for (FInteractionIsland* Island : Islands)
{
CheckInteractionThreadSafety(Island);
Island->Uninitialize(true);
}
}
struct FInteractionSearchContextGroup
{
bool Contains(const FInteractionSearchContext& SearchContext) const
{
for (int32 AnimContextIndex = 0; AnimContextIndex < SearchContext.Num(); ++AnimContextIndex)
{
if (AnimContextToTickPriority.Find(SearchContext.GetAnimContext(AnimContextIndex)))
{
return true;
}
}
return false;
}
void Add(const FInteractionSearchContext& SearchContext, int32 SearchContextIndex)
{
for (int32 AnimContextIndex = 0; AnimContextIndex < SearchContext.Num(); ++AnimContextIndex)
{
if (const UObject* AnimContext = SearchContext.GetAnimContext(AnimContextIndex))
{
if (int32* TickPriority = AnimContextToTickPriority.Find(AnimContext))
{
*TickPriority = FMath::Max(*TickPriority, SearchContext.TickPriorities[AnimContextIndex]);
}
else
{
AnimContextToTickPriority.Add(AnimContext) = SearchContext.TickPriorities[AnimContextIndex];
}
}
}
SearchContextsIndices.Add(SearchContextIndex);
}
void Merge(const FInteractionSearchContextGroup& SearchContextGroup)
{
for (const FAnimContextToTickPriorityPair& AnimContextToTickPriorityPair : SearchContextGroup.AnimContextToTickPriority)
{
if (int32* TickPriority = AnimContextToTickPriority.Find(AnimContextToTickPriorityPair.Key))
{
*TickPriority = FMath::Max(*TickPriority, AnimContextToTickPriorityPair.Value);
}
else
{
AnimContextToTickPriority.Add(AnimContextToTickPriorityPair.Key) = AnimContextToTickPriorityPair.Value;
}
}
for (int32 SearchContextsIndex : SearchContextGroup.SearchContextsIndices)
{
SearchContextsIndices.Add(SearchContextsIndex);
}
}
// all the AnimContexts in this group with their TickPriority
typedef TPair<const UObject*, int32> FAnimContextToTickPriorityPair;
typedef TMap<const UObject*, int32, TInlineSetAllocator<16, TMemStackSetAllocator<>>> FAnimContextToTickPriority;
FAnimContextToTickPriority AnimContextToTickPriority;
// indexes to the searchcontexts assigned to this group
TArray<int32, TInlineAllocator<16, TMemStackAllocator<>>> SearchContextsIndices;
};
// grouping SearchContexts AnimContext(s) in FInteractionSearchContextGroup(s). We'll create as many interaction islands as many groups
TArray<FInteractionSearchContextGroup, TInlineAllocator<PreallocatedSearchesNum, TMemStackAllocator<>>> SearchContextGroups;
for (int32 SearchContextIndex = 0; SearchContextIndex < SearchContexts.Num(); ++SearchContextIndex)
{
// evaluating where to place SearchContext..
const FInteractionSearchContext& SearchContext = SearchContexts[SearchContextIndex];
int32 MainSearchContextGroupIndex = INDEX_NONE;
for (int32 SearchContextGroupIndex = 0; SearchContextGroupIndex < SearchContextGroups.Num();)
{
// ..if SearchContextGroups[SearchContextGroupIndex] contains ANY of the AnimContexts from SearchContext..
if (SearchContextGroups[SearchContextGroupIndex].Contains(SearchContext))
{
if (MainSearchContextGroupIndex == INDEX_NONE)
{
// ..we add SearchContext to SearchContextGroups[SearchContextGroupIndex]
// and set MainSearchContextGroupIndex to SearchContextGroupIndex to know what is the group containing SearchContext, so..
MainSearchContextGroupIndex = SearchContextGroupIndex;
SearchContextGroups[MainSearchContextGroupIndex].Add(SearchContext, SearchContextIndex);
++SearchContextGroupIndex;
}
else
{
// ..in case SearchContext has already being inserted in MainSearchContextGroupIndex group
// we merge the newly found SearchContextGroups[SearchContextGroupIndex] to SearchContextGroups[MainSearchContextGroupIndex]
// (containing another of the the AnimContexts)
SearchContextGroups[MainSearchContextGroupIndex].Merge(SearchContextGroups[SearchContextGroupIndex]);
SearchContextGroups.RemoveAt(SearchContextGroupIndex);
}
}
else
{
++SearchContextGroupIndex;
}
}
if (MainSearchContextGroupIndex == INDEX_NONE)
{
SearchContextGroups.AddDefaulted_GetRef().Add(SearchContext, SearchContextIndex);
}
}
TArray<FInteractionSearchContextGroup::FAnimContextToTickPriorityPair, TInlineAllocator<16, TMemStackAllocator<>>> SortedByTickPriorityAnimContexts;
for (FInteractionSearchContextGroup& SearchContextGroup : SearchContextGroups)
{
// @todo: search for the most suitable island to reuse to avoid having to Uninitialize/RemoveTickDependencies and InjectToActor right away
FInteractionIsland& Island = GetAvailableIsland();
CheckInteractionThreadSafety(&Island);
// initializing the island with its assigned SearchContexts
bool bAreTickDependenciesRequired = false;
check(Island.GetSearchContexts().IsEmpty());
for (int32 SearchContextsIndex : SearchContextGroup.SearchContextsIndices)
{
const FInteractionSearchContext& SearchContext = SearchContexts[SearchContextsIndex];
// if there're at least two AnimContext(s) potentially interacting with each other
// (where the search involves 2+ characters) tick dependencies are required to be thread safe
bAreTickDependenciesRequired |= SearchContext.Num() > 1;
Island.AddSearchContext(SearchContext);
}
// sorting SearchContextGroup.AnimContextToTickPriority by TickPriority
// (using SortedByTickPriorityAnimContexts since SearchContextGroup.AnimContextToTickPriority it's a TMap)
SortedByTickPriorityAnimContexts.Reset();
SortedByTickPriorityAnimContexts.Reserve(SearchContextGroup.AnimContextToTickPriority.Num());
for (const FInteractionSearchContextGroup::FAnimContextToTickPriorityPair& AnimContextToTickPriorityPair : SearchContextGroup.AnimContextToTickPriority)
{
SortedByTickPriorityAnimContexts.Add(AnimContextToTickPriorityPair);
}
SortedByTickPriorityAnimContexts.Sort(
[](const FInteractionSearchContextGroup::FAnimContextToTickPriorityPair& A, const FInteractionSearchContextGroup::FAnimContextToTickPriorityPair& B)
{
return B.Value < A.Value;
});
// injecting tick dependencies between island AnimContext following their TickPriorities,
// so the AnimContext with the highest TickPriority will be elected as "Main Actor", being evaluated,
// and performing all the island searches, before any other Actor in the same island\
// (that'll end up using the cached search results in a multithread manner)
for (const FInteractionSearchContextGroup::FAnimContextToTickPriorityPair& SortedByTickPriorityAnimContext : SortedByTickPriorityAnimContexts)
{
Island.InjectToActor(SortedByTickPriorityAnimContext.Key, bAreTickDependenciesRequired);
}
}
}
#if DO_CHECK
bool UPoseSearchInteractionSubsystem::ValidateAllIslands() const
{
using namespace UE::PoseSearch;
TSet<TWeakObjectPtr<UActorComponent>> TickActorComponents;
typedef TSet<const UObject*> FIslandAnimContexts;
TArray<FIslandAnimContexts> IslandsAnimContexts;
const int32 NumIslands = Islands.Num();
IslandsAnimContexts.Reserve(NumIslands);
bool bAlreadyInSet = false;
for (const FInteractionIsland* Island : Islands)
{
for (const TWeakObjectPtr<UActorComponent>& TickActorComponent : Island->GetTickActorComponents())
{
TickActorComponents.Add(TickActorComponent, &bAlreadyInSet);
if (bAlreadyInSet)
{
return false;
}
}
FIslandAnimContexts& IslandAnimContexts = IslandsAnimContexts.AddDefaulted_GetRef();
for (const FInteractionSearchContext& SearchContext : Island->GetSearchContexts())
{
for (int32 AnimContextIndex = 0; AnimContextIndex < SearchContext.Num(); ++AnimContextIndex)
{
if (const UObject* AnimContext = SearchContext.GetAnimContext(AnimContextIndex))
{
IslandAnimContexts.Add(AnimContext);
}
}
}
}
for (int32 IslandIndex = 0; IslandIndex < NumIslands; ++IslandIndex)
{
for (const UObject* AnimContext : IslandsAnimContexts[IslandIndex])
{
for (int32 OtherIslandIndex = 0; OtherIslandIndex < NumIslands; ++OtherIslandIndex)
{
if (IslandIndex != OtherIslandIndex)
{
if (IslandsAnimContexts[OtherIslandIndex].Find(AnimContext))
{
// AnimContext is shared between multiple islands. it'd cause multi threadind issues!
return false;
}
}
}
}
}
return true;
}
#endif // DO_CHECK
void UPoseSearchInteractionSubsystem::PopulateContinuingProperties(float DeltaSeconds, TArrayView<UE::PoseSearch::FInteractionSearchContext> SearchContexts) const
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_UPoseSearchInteractionSubsystem_PopulateContinuingProperties);
using namespace UE::PoseSearch;
check(IsInGameThread());
for (FInteractionSearchContext& SearchContext : SearchContexts)
{
// searching this SearchContext in all the islands to initialize its continuing pose
for (const FInteractionIsland* Island : Islands)
{
if (const FSearchResult* SearchResult = Island->FindSearchResult(SearchContext))
{
// is still valid...
if (SearchResult->IsValid())
{
if (const UE::PoseSearch::FSearchIndexAsset* SearchIndexAsset = SearchResult->GetSearchIndexAsset())
{
if (const FPoseSearchDatabaseAnimationAssetBase* DatabaseAsset = SearchResult->Database->GetDatabaseAnimationAsset<FPoseSearchDatabaseAnimationAssetBase>(*SearchIndexAsset))
{
check(SearchIndexAsset->GetToRealTimeFactor() > UE_KINDA_SMALL_NUMBER);
// in case DatabaseAsset->GetAnimationAsset() is a blendspace, SearchResult->AssetTime is a normalized time in the interval [0,1]
// so we need to convert the delta time in seconds to the asset normalized time before integrating SearchResult->AssetTime
const float NormalizedDeltaTime = DeltaSeconds / SearchIndexAsset->GetToRealTimeFactor();
SearchContext.PlayingAssetAccumulatedTime = SearchResult->AssetTime + NormalizedDeltaTime;
SearchContext.PlayingAsset = DatabaseAsset->GetAnimationAsset();
SearchContext.bIsPlayingAssetMirrored = SearchIndexAsset->IsMirrored();
SearchContext.PlayingAssetBlendParameters = SearchIndexAsset->GetBlendParameters();
// @todo: populate SearchContext.InterruptMode
}
}
}
break;
}
}
}
}
UE::PoseSearch::FInteractionIsland* UPoseSearchInteractionSubsystem::FindIsland(const UObject* AnimContext, bool bCompareOwningActors)
{
using namespace UE::PoseSearch;
if (AnimContext)
{
if (bCompareOwningActors)
{
const AActor* Actor = GetContextOwningActor(AnimContext);
for (FInteractionIsland* Island : Islands)
{
for (const TWeakObjectPtr<const UObject>& IslandAnimContext : Island->GetIslandAnimContexts())
{
if (GetContextOwningActor(IslandAnimContext.Get()) == Actor)
{
return Island;
}
}
}
}
else
{
for (FInteractionIsland* Island : Islands)
{
if (Island->GetIslandAnimContexts().Contains(AnimContext))
{
return Island;
}
}
}
}
return nullptr;
}
UPoseSearchInteractionSubsystem* UPoseSearchInteractionSubsystem::GetSubsystem_AnyThread(const UObject* AnimContext)
{
if (AnimContext)
{
if (UWorld* World = AnimContext->GetWorld())
{
// We expect the subsystem to be already created from the GameThread.
// We don't create the subsystem from any thread
if (World->HasSubsystem<UPoseSearchInteractionSubsystem>())
{
return World->GetSubsystem<UPoseSearchInteractionSubsystem>();
}
}
}
return nullptr;
}
void UPoseSearchInteractionSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
check(IsInGameThread());
Super::Initialize(Collection);
}
void UPoseSearchInteractionSubsystem::Deinitialize()
{
UpdateValidInteractionSearches();
DestroyAllIslands();
Super::Deinitialize();
}
void UPoseSearchInteractionSubsystem::AddAvailabilities(const TArrayView<const FPoseSearchInteractionAvailability> Availabilities, const UObject* AnimContext, FName PoseHistoryName, const UE::PoseSearch::IPoseHistory* PoseHistory)
{
using namespace UE::PoseSearch;
check(AnimContext && AnimContext->GetWorld() && AnimContext->GetWorld() == GetWorld());
// collecting valid availabilities indexes here to minimize the time spent on the lock and avoid locking at all
// if there're actually no valid availabilities (as well as avoiding polluting AnimContextsAvailabilities with an empty entry).
// This is unfortunately a common setup, and that's the main reason we optimize against it
TArray<int32, TInlineAllocator<64>> ValidAvailabilityIndexes;
for (int32 AvailabilityIndex = 0; AvailabilityIndex < Availabilities.Num(); ++AvailabilityIndex)
{
const FPoseSearchInteractionAvailability& Availability = Availabilities[AvailabilityIndex];
if ((Availability.Database && Availability.Database->Schema) || Availability.IsTagValid())
{
ValidAvailabilityIndexes.Add(AvailabilityIndex);
}
}
if (!ValidAvailabilityIndexes.IsEmpty())
{
FScopeLock Lock(&AnimContextsAvailabilitiesMutex);
const int32 UpperBoundIndex = Algo::UpperBoundBy(AnimContextsAvailabilities, AnimContext,
[](const FPoseSearchInteractionAnimContextAvailabilities& AnimContextAvailabilities)
{
return AnimContextAvailabilities.AnimContext.Get();
},
[](const UObject* AnimContextA, const UObject* AnimContextB)
{
return AnimContextA < AnimContextB;
});
int32 AnimContextAvailabilityIndex;
if (UpperBoundIndex > 0 && AnimContextsAvailabilities[UpperBoundIndex - 1].AnimContext == AnimContext)
{
// we found the AnimContextsAvailabilities for AnimContext
AnimContextAvailabilityIndex = UpperBoundIndex - 1;
}
else
{
// we create a new AnimContextsAvailabilities for AnimContext, preserving AnimContextsAvailabilities sorting
AnimContextsAvailabilities.InsertDefaulted_GetRef(UpperBoundIndex).AnimContext = AnimContext;
AnimContextAvailabilityIndex = UpperBoundIndex;
}
for (const int32 AvailabilityIndex : ValidAvailabilityIndexes)
{
// Avoiding adding trivial duplicates. FPoseSearchInteractionAvailabilityEx could not be fully specified to understand if it's an actual duplicate in case the pose
// history is passed by name or the Availability.Database is null and supposed to be resolved using other availabilities Database(s) with the same Availability.Tag.
// The duplicated availabilities are excluded when creating the combinations of possible interactions during FAnimContextInfoVisitor when
// FRoledAnimContextInfos.AddRoledAnimContextInfos calls FRoledAnimContextInfos.AddUnique
FPoseSearchInteractionAvailabilityEx AvailabilityEx;
AvailabilityEx.Init(Availabilities[AvailabilityIndex], PoseHistoryName, PoseHistory);
AnimContextsAvailabilities[AnimContextAvailabilityIndex].Availabilities.AddUnique(AvailabilityEx);
}
}
}
void UPoseSearchInteractionSubsystem::GenerateAnimContextInfosAndTagToDatabases(UE::PoseSearch::FAnimContextInfos& AnimContextInfos, UE::PoseSearch::FTagToDatabases& TagToDatabases) const
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_UPoseSearchInteractionSubsystem_GenerateAnimContextInfos);
using namespace UE::PoseSearch;
const UWorld* SubsystemWorld = GetWorld();
check(SubsystemWorld);
check(AnimContextInfos.IsEmpty() && TagToDatabases.IsEmpty());
check(Algo::IsSorted(AnimContextsAvailabilities, [](const FPoseSearchInteractionAnimContextAvailabilities& A, const FPoseSearchInteractionAnimContextAvailabilities& B)
{
return A.AnimContext < B.AnimContext;
}));
for (const FPoseSearchInteractionAnimContextAvailabilities& AnimContextAvailabilities : AnimContextsAvailabilities)
{
check(AnimContextAvailabilities.AnimContext && AnimContextAvailabilities.AnimContext->GetWorld() && AnimContextAvailabilities.AnimContext->GetWorld() == GetWorld());
check(!AnimContextAvailabilities.Availabilities.IsEmpty());
// adding AnimContext to AnimContextsWithAvailabilities only if at least one availability has a valid database or has a valid tag
for (const FPoseSearchInteractionAvailabilityEx& InteractionAvailabilityEx : AnimContextAvailabilities.Availabilities)
{
bool bAnyValidAvailability = false;
if (const UPoseSearchDatabase* Database = InteractionAvailabilityEx.Database.Get())
{
check(Database->Schema);
if (InteractionAvailabilityEx.IsTagValid())
{
TagToDatabases.FindOrAdd(InteractionAvailabilityEx.Tag).AddUnique(InteractionAvailabilityEx.Database);
}
}
}
}
const int32 NumAnimContextInfos = AnimContextsAvailabilities.Num();
AnimContextInfos.SetNum(NumAnimContextInfos);
for (int32 AnimContextInfoIndex = 0; AnimContextInfoIndex < NumAnimContextInfos; ++AnimContextInfoIndex)
{
AnimContextInfos[AnimContextInfoIndex].Init(AnimContextsAvailabilities[AnimContextInfoIndex]);
}
// solving the broad phase using the AnimContextInfos
for (int32 AnimContextInfoIndexA = 0; AnimContextInfoIndexA < NumAnimContextInfos; ++AnimContextInfoIndexA)
{
for (int32 AnimContextInfoIndexB = AnimContextInfoIndexA + 1; AnimContextInfoIndexB < NumAnimContextInfos; ++AnimContextInfoIndexB)
{
if (AnimContextInfos[AnimContextInfoIndexA].CanInteractWith(AnimContextInfos[AnimContextInfoIndexB]))
{
// the AnimContext of AnimContextInfos[AnimContextInfoIndexA] can potentially interact with the one from AnimContextInfos[AnimContextInfoIndexB]:
// linking AnimContextInfos[AnimContextInfoIndexA] to AnimContextInfos[AnimContextInfoIndexA] and vice versa to keep track of this when evaluating the broad phase.
// Since AnimContextInfos does't reallocate anymore, it's safe to store pointers to internal elements of the array!
AnimContextInfos[AnimContextInfoIndexA].NearbyAnimContextInfos.Emplace(&AnimContextInfos[AnimContextInfoIndexB]);
AnimContextInfos[AnimContextInfoIndexB].NearbyAnimContextInfos.Emplace(&AnimContextInfos[AnimContextInfoIndexA]);
}
}
}
}
void UPoseSearchInteractionSubsystem::GenerateSearchContexts(float DeltaSeconds, UE::PoseSearch::FInteractionSearchContexts& SearchContexts) const
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_UPoseSearchInteractionSubsystem_GenerateSearchContexts);
using namespace UE::PoseSearch;
check(SearchContexts.IsEmpty());
struct FRoledAnimContextInfo
{
FRoledAnimContextInfo(const FPoseSearchInteractionAvailabilityEx& InAvailability, const FAnimContextInfo& InAnimContextInfo, const FRole InRole, const IPoseHistory* InPoseHistory, const UPoseSearchDatabase& InDatabase)
: Availability(&InAvailability)
, AnimContextInfo(&InAnimContextInfo)
, Role(InRole)
, PoseHistory(InPoseHistory)
, Database(&InDatabase)
{
}
// perform narrow phase analysis checking if the AnimContextInfo with the specialized properties from Availability, Role, PoseHistory, Database,
// can interact with OtherRoledAnimContextInfo. This is a less relaxed analysis than the one performed in FAnimContextInfo::CanInteractWith
bool CanInteractWith(const FRoledAnimContextInfo& OtherRoledAnimContextInfo, bool bWasSearchContextInteracting) const
{
check(this != &OtherRoledAnimContextInfo);
const FVector DeltaLocation = AnimContextInfo->Location - OtherRoledAnimContextInfo.AnimContextInfo->Location;
const float DistanceSquared = DeltaLocation.SquaredLength();
float MaxDistance;
if (bWasSearchContextInteracting)
{
MaxDistance = FMath::Min(Availability->BroadPhaseRadius + Availability->BroadPhaseRadiusIncrementOnInteraction, OtherRoledAnimContextInfo.Availability->BroadPhaseRadius + OtherRoledAnimContextInfo.Availability->BroadPhaseRadiusIncrementOnInteraction);
}
else
{
MaxDistance = FMath::Min(Availability->BroadPhaseRadius, OtherRoledAnimContextInfo.Availability->BroadPhaseRadius);
}
const float MaxDistanceSquared = MaxDistance * MaxDistance;
return DistanceSquared <= MaxDistanceSquared;
}
bool operator==(const FRoledAnimContextInfo& Other) const = default;
// Availability that spawned this FRoledAnimContextInfo
const FPoseSearchInteractionAvailabilityEx* Availability = nullptr;
// AnimContextInfo containing all the information regarding the AnimContext that spawned this FRoledAnimContextInfo,
// including all the availabilities associated to the AnimContext as well as all the other AnimContext(s) it can potentially interact with
const FAnimContextInfo* AnimContextInfo = nullptr;
const FRole Role = DefaultRole;
const IPoseHistory* PoseHistory = nullptr;
const UPoseSearchDatabase* Database = nullptr;
};
struct FRoledAnimContextInfos : TArray<FRoledAnimContextInfo, TInlineAllocator<PreallocatedRolesNum, TMemStackAllocator<>>>
{
void AddRoledAnimContextInfos(const FPoseSearchInteractionAvailabilityEx& Availability, const FAnimContextInfo& AnimContextInfo, const IPoseHistory* PoseHistory, const UPoseSearchDatabase& Database)
{
const UPoseSearchSchema* Schema = Database.Schema;
check(Schema);
if (Availability.RolesFilter.IsEmpty())
{
// adding ALL the possible roles from the database:
for (const FPoseSearchRoledSkeleton& RoledSkeleton : Schema->GetRoledSkeletons())
{
AddUnique(FRoledAnimContextInfo(Availability, AnimContextInfo, RoledSkeleton.Role, PoseHistory, Database));
}
}
else
{
for (const FRole& Role : Availability.RolesFilter)
{
if (Schema->GetRoledSkeleton(Role))
{
AddUnique(FRoledAnimContextInfo(Availability, AnimContextInfo, Role, PoseHistory, Database));
}
else
{
UE_LOG(LogPoseSearch, Warning, TEXT("UPoseSearchInteractionSubsystem::GenerateSearchContexts unsupported Role %s for Database %s"), *Role.ToString(), *Database.GetName());
}
}
}
}
};
// visits the FAnimContextInfos recursively to identify groups of nearby AnimContextInfo(s), relying on FAnimContextInfo::NearbyAnimContextInfos information.
// it calls OnNewAnimAnimContextInfoFound on every new FAnimContextInfo found/visited, and OnDoneGroupingAnimContexts once reaches the end of the current group AnimContext(s).
// it's then restart calling OnNewAnimAnimContextInfoFound in case there are still unvisited FAnimContextInfo(s), untill it visited ALL the FAnimContextInfo(s) in the input FAnimContextInfos
struct FAnimContextInfoVisitor
{
FAnimContextInfoVisitor(
const FAnimContextInfos& AnimContextInfos,
TFunctionRef<void(const FAnimContextInfo&)> OnNewAnimAnimContextInfoFound,
TFunctionRef<void()> OnDoneGroupingAnimContexts)
{
for (const FAnimContextInfo& AnimContextInfo : AnimContextInfos)
{
if (AnimContextInfo.NearbyAnimContextInfos.IsEmpty())
{
check(!VisitedAnimContextInfos.Find(&AnimContextInfo));
// no need to add this context to the VisitedAnimContextInfos since it's isolated!
OnNewAnimAnimContextInfoFound(AnimContextInfo);
OnDoneGroupingAnimContexts();
}
else if(!VisitedAnimContextInfos.Find(&AnimContextInfo))
{
// starting the evaluation of a new set of grouped AnimContext(s)
// processing the AnimContextsAvailabilities of the current AnimContextArray to fill up a map of Databases pointing to an array of all the AnimContexts with related roles
VisitRecursively(AnimContextInfo, OnNewAnimAnimContextInfoFound);
OnDoneGroupingAnimContexts();
}
}
}
private:
void VisitRecursively(const FAnimContextInfo& AnimContextInfoToVisit, TFunctionRef<void(const FAnimContextInfo&)> OnNewAnimAnimContextInfoFound)
{
check(!AnimContextInfoToVisit.NearbyAnimContextInfos.IsEmpty());
bool bIsAlreadyInSet;
VisitedAnimContextInfos.FindOrAdd(&AnimContextInfoToVisit, &bIsAlreadyInSet);
if (!bIsAlreadyInSet)
{
OnNewAnimAnimContextInfoFound(AnimContextInfoToVisit);
for (const FAnimContextInfo* NearbyAnimContextInfo : AnimContextInfoToVisit.NearbyAnimContextInfos)
{
check(NearbyAnimContextInfo);
VisitRecursively(*NearbyAnimContextInfo, OnNewAnimAnimContextInfoFound);
}
}
}
TSet<const FAnimContextInfo*, DefaultKeyFuncs<const FAnimContextInfo*>, TInlineSetAllocator<32, TMemStackSetAllocator<>>> VisitedAnimContextInfos;
};
// caching AnimContext(s) locations, max broad phase radiuses (squared) and collect relations of possible interactions between AnimContext(s)
// (stored in FAnimContextInfo::NearbyAnimContextInfos::Index) as fast broad phase evaluation refined later on during FInteractionSearchContexts generation
// and generating a mapping between availabilities Tag(s) to availabilities published databases
FAnimContextInfos AnimContextInfos;
FTagToDatabases TagToDatabases;
GenerateAnimContextInfosAndTagToDatabases(AnimContextInfos, TagToDatabases);
const TConstArrayView<FValidInteractionSearch> PreviousValidInteractionSearches = ValidInteractionSearches;
TMap<const UPoseSearchDatabase*, FRoledAnimContextInfos, TInlineSetAllocator<PreallocatedSearchesNum, TMemStackSetAllocator<>>> DatabaseToRoledAnimContextInfos;
// visiting ALL the AnimContexts in AnimContextInfos and relying on the cached information to refine potential interactions
FAnimContextInfoVisitor AnimContextInfoVisitor(AnimContextInfos,
// OnNewAnimContextFound: called when the FAnimContextInfoVisitor finds a new AnimContext that can be grouped in the current group of possibly interacting AnimContext(s)
[&TagToDatabases, &DatabaseToRoledAnimContextInfos](const FAnimContextInfo& AnimContextInfo)
{
// analyzing all the Availability(s) associated with this AnimContext and eventually generate the associated FRoledAnimContextInfos,
// inserted in a per database sorted data structure (DatabaseToRoledAnimContextInfos)
check(AnimContextInfo.AnimContextAvailabilities);
for (const FPoseSearchInteractionAvailabilityEx& Availability : AnimContextInfo.AnimContextAvailabilities->Availabilities)
{
if (const IPoseHistory* PoseHistory = Availability.GetPoseHistory(AnimContextInfo.AnimContextAvailabilities->AnimContext))
{
if (const UPoseSearchDatabase* Database = Availability.Database.Get())
{
check(Database->Schema);
FRoledAnimContextInfos& RoledAnimContextInfos = DatabaseToRoledAnimContextInfos.FindOrAdd(Database);
RoledAnimContextInfos.AddRoledAnimContextInfos(Availability, AnimContextInfo, PoseHistory, *Database);
}
else if (Availability.IsTagValid())
{
// since Database is null, but this availability has a valid Tag, we're looking for valid databases by Availability.Tag
if (const FDatabasesPerTag* DatabasesPerTag = TagToDatabases.Find(Availability.Tag))
{
check(!DatabasesPerTag->IsEmpty());
for (const UPoseSearchDatabase* DatabaseFromTag : *DatabasesPerTag)
{
check(DatabaseFromTag && DatabaseFromTag->Schema);
FRoledAnimContextInfos& RoledAnimContextInfos = DatabaseToRoledAnimContextInfos.FindOrAdd(DatabaseFromTag);
RoledAnimContextInfos.AddRoledAnimContextInfos(Availability, AnimContextInfo, PoseHistory, *DatabaseFromTag);
}
}
else
{
//@todo: should we add a verbose LOG here? not sure since it'd be very spammy...
// this is a valid condition we shouldn't log: for example when the "main character" is loaded and publishing availabilities with a valid tag and null database,
// looking for other NPC / seconday characters to interact with, but they are not present of didn't publish any availability
}
}
else
{
UE_LOG(LogPoseSearch, Log, TEXT("UPoseSearchInteractionSubsystem::GenerateSearchContexts null Availability.Database (with invalid Availability.Tag)"));
}
}
else
{
UE_LOG(LogPoseSearch, Error, TEXT("UPoseSearchInteractionSubsystem::GenerateSearchContexts couldn't find PoseHistory %s for AnimContext %s"), *Availability.GetPoseHistoryName(), *AnimContextInfo.AnimContextAvailabilities->AnimContext->GetName());
}
}
},
// OnDoneGroupingAnimContexts: called when the FAnimContextInfoVisitor reaches the end of a group of possibly interacting AnimContext(s)
[&DatabaseToRoledAnimContextInfos, &SearchContexts, &PreviousValidInteractionSearches]()
{
// for each database now we try to create all the possible combinations of the roled anim instances
// for example, given a database set up with assets for 2 characters interactions with roles RoleA and RoleB
// and 2 anim instances, all of them willing to partecipate in the 2 characters interaction with both roles RoleA and RoleB:
// CharA could be taking RoleA and RoleB,
// CharB could be taking RoleA and RoleB,
// we generate all the combinations from the array of options:
// CharA/RoleA, CharA/RoleB, CharB/RoleA, CharB/RoleB
//
// and we prune the invalid tuples:
//
// CharA/RoleA - CharA/RoleB -> invalid because of same CharA
// CharA/RoleA - CharB/RoleA -> invalid because of same RoleA
// CharA/RoleA - CharB/RoleB -> VALID!
//
// CharA/RoleB - CharB/RoleA -> VALID!
// CharA/RoleB - CharB/RoleB -> invalid because of same RoleB
//
// CharB/RoleA - CharB/RoleB -> invalid because of same CharB
for (TPair<const UPoseSearchDatabase*, FRoledAnimContextInfos>& DatabaseToRoledAnimContextInfosPair : DatabaseToRoledAnimContextInfos)
{
const UPoseSearchDatabase* Database = DatabaseToRoledAnimContextInfosPair.Key;
check(Database->Schema);
const TArray<FPoseSearchRoledSkeleton>& RoledSkeletons = Database->Schema->GetRoledSkeletons();
const int32 CombinationCardinality = RoledSkeletons.Num();
FRoledAnimContextInfos& RoledAnimContextInfos = DatabaseToRoledAnimContextInfosPair.Value;
// sort RoledAnimContextInfos to generate deterministic SearchContext across multiple frames!
RoledAnimContextInfos.Sort([](const FRoledAnimContextInfo& RoledAnimContextInfoA, const FRoledAnimContextInfo& RoledAnimContextInfoB)
{
return RoledAnimContextInfoA.AnimContextInfo->AnimContextAvailabilities->AnimContext < RoledAnimContextInfoB.AnimContextInfo->AnimContextAvailabilities->AnimContext;
});
GenerateCombinations(RoledAnimContextInfos.Num(), CombinationCardinality,
// Combination is an array of indexes in RoledAnimContextInfos: 0 <= Combination[i] < RoledAnimContextInfos.Num()
[Database, &RoledSkeletons, &RoledAnimContextInfos, &SearchContexts, &PreviousValidInteractionSearches](const TConstArrayView<int32> Combination)
{
// CombinationCardinality represents the number of roles as well as the number interacting AnimContext(s) (ultimately number of Characters involved in the interaction)
const int32 CombinationCardinality = Combination.Num();
TSet<const UObject*, DefaultKeyFuncs<const UObject*>, TInlineSetAllocator<PreallocatedRolesNum, TMemStackSetAllocator<>>> UniqueAnimContexts;
for (int32 CombinationIndex = 0; CombinationIndex < CombinationCardinality; ++CombinationIndex)
{
const int32 RoledAnimContextIndex = Combination[CombinationIndex];
const FRoledAnimContextInfo& RoledAnimContextInfo = RoledAnimContextInfos[RoledAnimContextIndex];
check(RoledAnimContextInfo.AnimContextInfo);
bool bIsAlreadyInSet;
UniqueAnimContexts.Add(RoledAnimContextInfo.AnimContextInfo->AnimContextAvailabilities->AnimContext, &bIsAlreadyInSet);
if (bIsAlreadyInSet)
{
// we have a duplicate AnimContext. this combination is NOT valid
return false;
}
}
FInteractionSearchContext SearchContext;
SearchContext.Database = Database;
// setting up a FRoledAnimContextInfo in RoledAnimContextInfos describing
// this potential interaction properties about how to perform the search
for (int32 CombinationIndex = 0; CombinationIndex < CombinationCardinality; ++CombinationIndex)
{
const int32 RoledAnimContextIndex = Combination[CombinationIndex];
const FRoledAnimContextInfo& RoledAnimContextInfo = RoledAnimContextInfos[RoledAnimContextIndex];
check(RoledAnimContextInfo.PoseHistory && RoledAnimContextInfo.Availability);
SearchContext.Add(RoledAnimContextInfo.AnimContextInfo->AnimContextAvailabilities->AnimContext, RoledAnimContextInfo.PoseHistory, RoledAnimContextInfo.Role);
SearchContext.bDisableCollisions |= RoledAnimContextInfo.Availability->bDisableCollisions;
SearchContext.TickPriorities.Add(RoledAnimContextInfo.Availability->TickPriority);
#if ENABLE_ANIM_DEBUG
SearchContext.DebugAvailabilities.Add(*RoledAnimContextInfo.Availability);
#endif // ENABLE_ANIM_DEBUG
}
// does SearchContext cover all the roles required by this interaction?
for (const FPoseSearchRoledSkeleton& RoledSkeleton : RoledSkeletons)
{
// CombinationCardinality is usually 2-3, so we can search the SearchContext.Roles array for duplicates without requiring a faster container like TSet
if (!SearchContext.GetRoles().Contains(RoledSkeleton.Role))
{
return false;
}
}
// looking for a preexisting valid interaction resmbling SearchContext
for (const FValidInteractionSearch& PreviousValidInteractionSearch : PreviousValidInteractionSearches)
{
if (PreviousValidInteractionSearch.SearchContext.IsEquivalent(SearchContext))
{
SearchContext.bIsContinuingInteraction = true;
break;
}
}
// checking if this combination is valid for the Database:
for (int32 CombinationIndex = 0; CombinationIndex < CombinationCardinality; ++CombinationIndex)
{
const int32 RoledAnimContextIndex = Combination[CombinationIndex];
const FRoledAnimContextInfo& RoledAnimContextInfo = RoledAnimContextInfos[RoledAnimContextIndex];
// checking the narrow phase!
for (int32 OtherCombinationIndex = CombinationIndex + 1; OtherCombinationIndex < CombinationCardinality; ++OtherCombinationIndex)
{
const int32 OtherRoledAnimContextIndex = Combination[OtherCombinationIndex];
const FRoledAnimContextInfo& OtherRoledAnimContextInfo = RoledAnimContextInfos[OtherRoledAnimContextIndex];
// if any of the RoledAnimContextInfo cannot interact with any OtherRoledAnimContextInfo the inteaction cannot happen!
if (!RoledAnimContextInfo.CanInteractWith(OtherRoledAnimContextInfo, SearchContext.bIsContinuingInteraction))
{
return false;
}
}
}
#if DO_CHECK
for (const FInteractionSearchContext& ContainedSearchContext : SearchContexts)
{
check(!ContainedSearchContext.IsEquivalent(SearchContext));
}
check(SearchContext.CheckForConsistency());
#endif // DO_CHECK
SearchContexts.Emplace(SearchContext);
return true;
});
}
// done using DatabaseToRoledAnimContextInfos. clearing up for the next group of AnimContext(s)
DatabaseToRoledAnimContextInfos.Reset();
});
// populating the continuing pose properties for the SearchContexts from the current Islands
PopulateContinuingProperties(DeltaSeconds, SearchContexts);
}
void UPoseSearchInteractionSubsystem::OnInteractionStart(UE::PoseSearch::FValidInteractionSearch& ValidInteractionSearch)
{
using namespace UE::PoseSearch;
#if ENABLE_VISUAL_LOG
ValidInteractionSearch.SearchContext.VLogContext(FColor::Blue);
#endif
check(ValidInteractionSearch.DisabledCollisions.IsEmpty());
if (ValidInteractionSearch.SearchContext.bDisableCollisions)
{
TArray<AActor*, TInlineAllocator<PreallocatedRolesNum, TMemStackAllocator<>>> Actors;
TArray<UPrimitiveComponent*, TInlineAllocator<PreallocatedRolesNum, TMemStackAllocator<>>> PrimitiveComponents;
for (int32 AnimContextIndex = 0; AnimContextIndex < ValidInteractionSearch.SearchContext.Num(); ++AnimContextIndex)
{
if (const UObject* AnimContext = ValidInteractionSearch.SearchContext.GetAnimContext(AnimContextIndex))
{
AActor* Actor = const_cast<AActor*>(GetContextOwningActor(AnimContext));
check(Actor);
Actors.Add(Actor);
PrimitiveComponents.Add(Cast<UPrimitiveComponent>(Actor->GetRootComponent()));
}
}
for (int32 IndexA = 0; IndexA < Actors.Num(); ++IndexA)
{
for (int32 IndexB = IndexA + 1; IndexB < Actors.Num(); ++IndexB)
{
AActor* ActorA = Actors[IndexA];
AActor* ActorB = Actors[IndexB];
check(ActorA && ActorB);
UPrimitiveComponent* PrimitiveComponentA = PrimitiveComponents[IndexA];
UPrimitiveComponent* PrimitiveComponentB = PrimitiveComponents[IndexB];
if (PrimitiveComponentA && !PrimitiveComponentA->GetMoveIgnoreActors().Contains(ActorB))
{
ValidInteractionSearch.DisabledCollisions.Add({ ActorA, ActorB });
PrimitiveComponentA->IgnoreActorWhenMoving(ActorB, true);
}
if (PrimitiveComponentB && !PrimitiveComponentB->GetMoveIgnoreActors().Contains(ActorA))
{
ValidInteractionSearch.DisabledCollisions.Add({ ActorB, ActorA });
PrimitiveComponentB->IgnoreActorWhenMoving(ActorA, true);
}
}
}
}
}
void UPoseSearchInteractionSubsystem::OnInteractionContinuing(UE::PoseSearch::FValidInteractionSearch& ValidInteractionSearch)
{
#if ENABLE_VISUAL_LOG
ValidInteractionSearch.SearchContext.VLogContext(FColor::Green);
#endif
}
void UPoseSearchInteractionSubsystem::OnInteractionEnd(UE::PoseSearch::FValidInteractionSearch& ValidInteractionSearch)
{
using namespace UE::PoseSearch;
#if ENABLE_VISUAL_LOG
ValidInteractionSearch.SearchContext.VLogContext(FColor::Black);
#endif
for (const FDisabledCollisions::ElementType& DisabledCollision : ValidInteractionSearch.DisabledCollisions)
{
if (AActor* ActorA = DisabledCollision.Key.Get())
{
if (AActor* ActorB = DisabledCollision.Value.Get())
{
if (UPrimitiveComponent* PrimitiveComponentA = Cast<UPrimitiveComponent>(ActorA->GetRootComponent()))
{
PrimitiveComponentA->IgnoreActorWhenMoving(ActorB, false);
}
}
}
}
}
void UPoseSearchInteractionSubsystem::UpdateValidInteractionSearches()
{
using namespace UE::PoseSearch;
const int32 ValidInteractionSearchesNum = ValidInteractionSearches.Num();
TArray<bool, TInlineAllocator<PreallocatedSearchesNum * 4>> Visited;
Visited.SetNum(ValidInteractionSearchesNum);
TArray<FValidInteractionSearch, TInlineAllocator<PreallocatedSearchesNum * 4>> NewValidInteractionSearches;
for (FInteractionIsland* Island : Islands)
{
if (Island->IsInitialized())
{
// analyzing ALL current tick interaction results
for (const FInteractionSearchResult& SearchResult : Island->GetSearchResults())
{
const FInteractionSearchContext& SearchContext = Island->GetSearchContexts()[SearchResult.SearchIndex];
int32 Index = 0;
for (; Index < ValidInteractionSearchesNum; ++Index)
{
if (ValidInteractionSearches[Index].SearchContext.IsEquivalent(SearchContext))
{
check(!Visited[Index]);
Visited[Index] = true;
OnInteractionContinuing(ValidInteractionSearches[Index]);
break;
}
}
if (Index == ValidInteractionSearchesNum)
{
// we haven't found an equivalent SearchContext in ValidInteractionSearches, so it's a new interaction!
FValidInteractionSearch& NewValidInteractionSearch = NewValidInteractionSearches.AddDefaulted_GetRef();
NewValidInteractionSearch.SearchContext = SearchContext;
OnInteractionStart(NewValidInteractionSearch);
}
}
}
}
// checking for leftover unvisited ValidInteractionSearches. Those are interactions that just ended
for (int32 Index = 0; Index < ValidInteractionSearchesNum; ++Index)
{
if (Visited[Index])
{
NewValidInteractionSearches.Add(ValidInteractionSearches[Index]);
}
else
{
OnInteractionEnd(ValidInteractionSearches[Index]);
}
}
ValidInteractionSearches = NewValidInteractionSearches;
}
void UPoseSearchInteractionSubsystem::Tick(float DeltaSeconds)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_UPoseSearchInteractionSubsystem_Tick);
using namespace UE::PoseSearch;
Super::Tick(DeltaSeconds);
FMemMark Mark(FMemStack::Get());
UpdateValidInteractionSearches();
if (AnimContextsAvailabilities.IsEmpty())
{
bool bAllUninjected = true;
for (FInteractionIsland* Island : Islands)
{
if (Island->IsInitialized())
{
bAllUninjected = false;
}
}
if (bAllUninjected)
{
// nothing to do. early out
return;
}
}
check(IsInGameThread());
RegenerateAllIslands(DeltaSeconds);
// clearing up AnimContextsAvailabilities for the next frame
AnimContextsAvailabilities.Reset();
#if DO_CHECK
check(ValidateAllIslands());
#endif
}
TStatId UPoseSearchInteractionSubsystem::GetStatId() const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(UPoseSearchInteractionSubsystem, STATGROUP_Tickables);
}
void UPoseSearchInteractionSubsystem::Query_AnyThread(const TArrayView<const FPoseSearchInteractionAvailability> Availabilities, const UObject* AnimContext,
FPoseSearchBlueprintResult& Result, FName PoseHistoryName, const UE::PoseSearch::IPoseHistory* PoseHistory, bool bValidateResultAgainstAvailabilities)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_UPoseSearchInteractionSubsystem_Query_AnyThread);
using namespace UE::PoseSearch;
Result = FPoseSearchBlueprintResult();
#if !NO_CVARS
if (!GVarPoseSearchInteractionEnabled)
{
return;
}
#endif // !NO_CVARS
// if we find AnimContext in an island, we perform ALL the Island motion matching searches.
if (FInteractionIsland* Island = FindIsland(AnimContext))
{
Island->DoSearch_AnyThread(AnimContext, ValidInteractionSearches, Result);
if (bValidateResultAgainstAvailabilities && Result.SelectedAnim)
{
bool bResultValidated = false;
for (const FPoseSearchInteractionAvailability& Availability : Availabilities)
{
const bool bIsDatabaseValidates = (Availability.IsTagValid() && !Availability.Database) || (Availability.Database == Result.SelectedDatabase);
if (bIsDatabaseValidates && (Availability.RolesFilter.IsEmpty() || Availability.RolesFilter.Contains(Result.Role)))
{
bResultValidated = true;
break;
}
}
if (!bResultValidated)
{
Result = FPoseSearchBlueprintResult();
}
}
}
// queuing the availabilities for the next frame Query_AnyThread
AddAvailabilities(Availabilities, AnimContext, PoseHistoryName, PoseHistory);
}
void UPoseSearchInteractionSubsystem::GetResult_AnyThread(const UObject* AnimContext, FPoseSearchBlueprintResult& Result, bool bCompareOwningActors)
{
using namespace UE::PoseSearch;
if (FInteractionIsland* Island = FindIsland(AnimContext, bCompareOwningActors))
{
Island->GetResult_AnyThread(AnimContext, Result, bCompareOwningActors);
}
else
{
Result = FPoseSearchBlueprintResult();
}
}
#if ENABLE_ANIM_DEBUG
void UPoseSearchInteractionSubsystem::DebugDrawIslands() const
{
#if ENABLE_VISUAL_LOG
using namespace UE::PoseSearch;
check(IsInGameThread());
if (!FVisualLogger::IsRecording())
{
return;
}
static const FColor Colors[] =
{
FColor::White,
FColor::Black,
FColor::Red,
FColor::Green,
FColor::Blue,
FColor::Yellow,
FColor::Cyan,
FColor::Magenta,
FColor::Orange,
FColor::Purple,
FColor::Turquoise,
FColor::Silver,
FColor::Emerald
};
static const int32 NumColors = sizeof(Colors) / sizeof(Colors[0]);
int32 CurrentColorIndex = 0;
TArray<const UObject*, TInlineAllocator<256>> AllAnimContexts;
for (const FInteractionIsland* Island : Islands)
{
for (const TWeakObjectPtr<const UObject>& IslandAnimContextPtr : Island->GetIslandAnimContexts())
{
if (const UObject* IslandAnimContext = IslandAnimContextPtr.Get())
{
AllAnimContexts.Add(IslandAnimContext);
}
}
}
for (const FInteractionIsland* Island : Islands)
{
if (Island->IsInitialized())
{
const FColor& Color = Colors[CurrentColorIndex];
for (const FInteractionSearchContext& SearchContext : Island->GetSearchContexts())
{
for (int32 Index = 0; Index < SearchContext.Num(); ++Index)
{
if (const UObject* AnimContext = SearchContext.GetAnimContext(Index))
{
const FPoseSearchInteractionAvailability& DebugAvailability = SearchContext.DebugAvailabilities[Index];
float MaxBroadPhaseRadius;
if (SearchContext.bIsContinuingInteraction)
{
MaxBroadPhaseRadius = DebugAvailability.BroadPhaseRadius + DebugAvailability.BroadPhaseRadiusIncrementOnInteraction;
}
else
{
MaxBroadPhaseRadius = DebugAvailability.BroadPhaseRadius;
}
if (MaxBroadPhaseRadius > UE_SMALL_NUMBER)
{
const FTransform& Transform = GetContextTransform(AnimContext);
static const TCHAR* LogName = TEXT("PoseSearchInteraction");
for (const UObject* IslandAnimContext : AllAnimContexts)
{
UE_VLOG_CIRCLE(IslandAnimContext, LogName, Display, Transform.GetLocation(), FVector::UpVector, MaxBroadPhaseRadius, Color, TEXT(""));
}
if (!Island->HasTickDependencies())
{
const FVector ForwardAxisStart = Transform.TransformPosition(FVector::ForwardVector * MaxBroadPhaseRadius);
const FVector ForwardAxisEnd = Transform.TransformPosition(FVector::ForwardVector * -MaxBroadPhaseRadius);
const FVector LeftAxisStart = Transform.TransformPosition(FVector::LeftVector * MaxBroadPhaseRadius);
const FVector LeftAxisEnd = Transform.TransformPosition(FVector::LeftVector * -MaxBroadPhaseRadius);
for (const UObject* IslandAnimContext : AllAnimContexts)
{
UE_VLOG_SEGMENT(IslandAnimContext, LogName, Display, ForwardAxisStart, ForwardAxisEnd, Color, TEXT(""));
UE_VLOG_SEGMENT(IslandAnimContext, LogName, Display, LeftAxisStart, LeftAxisEnd, Color, TEXT(""));
}
}
}
}
}
}
CurrentColorIndex = (CurrentColorIndex + 1) % NumColors;
}
}
#endif // ENABLE_VISUAL_LOG
}
void UPoseSearchInteractionSubsystem::DebugLogTickDependencies() const
{
#if !NO_CVARS
using namespace UE::PoseSearch;
if (GVarPoseSearchInteractionLoglandsTickDependencies)
{
UE_LOG(LogPoseSearch, Log, TEXT("=================================================================="));
for (const FInteractionIsland* Island : Islands)
{
if (Island->IsInitialized())
{
Island->LogTickDependencies();
}
}
}
#endif // !NO_CVARS
}
#endif // ENABLE_ANIM_DEBUG