// 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>> NearbyAnimContextInfos; }; struct FAnimContextInfos : TArray> {}; typedef TArray>> FDatabasesPerTag; struct FTagToDatabases : TMap>> {}; } // 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(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 FAnimContextToTickPriorityPair; typedef TMap>> FAnimContextToTickPriority; FAnimContextToTickPriority AnimContextToTickPriority; // indexes to the searchcontexts assigned to this group TArray>> SearchContextsIndices; }; // grouping SearchContexts AnimContext(s) in FInteractionSearchContextGroup(s). We'll create as many interaction islands as many groups TArray>> 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>> 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> TickActorComponents; typedef TSet FIslandAnimContexts; TArray IslandsAnimContexts; const int32 NumIslands = Islands.Num(); IslandsAnimContexts.Reserve(NumIslands); bool bAlreadyInSet = false; for (const FInteractionIsland* Island : Islands) { for (const TWeakObjectPtr& 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 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(*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& 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()) { return World->GetSubsystem(); } } } return nullptr; } void UPoseSearchInteractionSubsystem::Initialize(FSubsystemCollectionBase& Collection) { check(IsInGameThread()); Super::Initialize(Collection); } void UPoseSearchInteractionSubsystem::Deinitialize() { UpdateValidInteractionSearches(); DestroyAllIslands(); Super::Deinitialize(); } void UPoseSearchInteractionSubsystem::AddAvailabilities(const TArrayView 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> 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>> { 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 OnNewAnimAnimContextInfoFound, TFunctionRef 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 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, 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 PreviousValidInteractionSearches = ValidInteractionSearches; TMap>> 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& DatabaseToRoledAnimContextInfosPair : DatabaseToRoledAnimContextInfos) { const UPoseSearchDatabase* Database = DatabaseToRoledAnimContextInfosPair.Key; check(Database->Schema); const TArray& 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 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, TInlineSetAllocator>> 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>> Actors; TArray>> PrimitiveComponents; for (int32 AnimContextIndex = 0; AnimContextIndex < ValidInteractionSearch.SearchContext.Num(); ++AnimContextIndex) { if (const UObject* AnimContext = ValidInteractionSearch.SearchContext.GetAnimContext(AnimContextIndex)) { AActor* Actor = const_cast(GetContextOwningActor(AnimContext)); check(Actor); Actors.Add(Actor); PrimitiveComponents.Add(Cast(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(ActorA->GetRootComponent())) { PrimitiveComponentA->IgnoreActorWhenMoving(ActorB, false); } } } } } void UPoseSearchInteractionSubsystem::UpdateValidInteractionSearches() { using namespace UE::PoseSearch; const int32 ValidInteractionSearchesNum = ValidInteractionSearches.Num(); TArray> Visited; Visited.SetNum(ValidInteractionSearchesNum); TArray> 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 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> AllAnimContexts; for (const FInteractionIsland* Island : Islands) { for (const TWeakObjectPtr& 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