// Copyright Epic Games, Inc. All Rights Reserved. #include "NavigationDataHandler.h" #include "AI/Navigation/NavigationDirtyElement.h" #include "AI/Navigation/NavRelevantInterface.h" #include "Engine/Level.h" #include "GameFramework/Actor.h" #include "NavAreas/NavArea.h" #include "NavMesh/RecastGeometryExport.h" #include "VisualLogger/VisualLogger.h" #if WITH_RECAST #include "DetourCrowd/DetourCrowd.h" #endif // WITH_RECAST DEFINE_LOG_CATEGORY_STATIC(LogNavOctree, Warning, All); namespace UE::NavigationHelper::Private { ENavigationDirtyFlag GetDirtyFlag(const int32 UpdateFlags, const ENavigationDirtyFlag DefaultValue) { return ((UpdateFlags & FNavigationOctreeController::OctreeUpdate_Geometry) != 0) ? ENavigationDirtyFlag::All : ((UpdateFlags & FNavigationOctreeController::OctreeUpdate_Modifiers) != 0) ? ENavigationDirtyFlag::DynamicModifier : DefaultValue; } } FNavigationDataHandler::FNavigationDataHandler(FNavigationOctreeController& InOctreeController, FNavigationDirtyAreasController& InDirtyAreasController) : OctreeController(InOctreeController), DirtyAreasController(InDirtyAreasController) {} void FNavigationDataHandler::ConstructNavOctree(const FVector& Origin, const double Radius, const ENavDataGatheringModeConfig DataGatheringMode, const float GatheringNavModifiersWarningLimitTime) { UE_LOG(LogNavOctree, Log, TEXT("CREATE (Origin:%s Radius:%.2f)"), *Origin.ToString(), Radius); OctreeController.Reset(); OctreeController.NavOctree = MakeShareable(new FNavigationOctree(Origin, Radius)); OctreeController.NavOctree->SetDataGatheringMode(DataGatheringMode); #if !UE_BUILD_SHIPPING OctreeController.NavOctree->SetGatheringNavModifiersTimeLimitWarning(GatheringNavModifiersWarningLimitTime); #endif // !UE_BUILD_SHIPPING } // Deprecated void FNavigationDataHandler::RemoveNavOctreeElementId(const FOctreeElementId2& ElementId, const int32 UpdateFlags) { RemoveFromNavOctree(ElementId, UpdateFlags); } void FNavigationDataHandler::RemoveFromNavOctree(const FOctreeElementId2& ElementId, const int32 UpdateFlags) { if (ensure(OctreeController.IsValidElement(ElementId))) { const FNavigationOctreeElement& ElementData = OctreeController.NavOctree->GetElementById(ElementId); // mark area occupied by given element as dirty except if explicitly set to skip this default behavior if (!ElementData.Data->bShouldSkipDirtyAreaOnAddOrRemove) { const ENavigationDirtyFlag DirtyFlag = UE::NavigationHelper::Private::GetDirtyFlag(UpdateFlags, ElementData.Data->GetDirtyFlag()); DirtyAreasController.AddArea( ElementData.Bounds.GetBox(), DirtyFlag, [&ElementData] { return ElementData.Data->SourceElement; }, /*DirtyElement*/nullptr, "Remove from navoctree"); } OctreeController.RemoveNode(ElementId, ElementData.Data.Get().SourceElement.Get().GetHandle()); } } // Deprecated FSetElementId FNavigationDataHandler::RegisterNavOctreeElement(UObject& ElementOwner, INavRelevantInterface& ElementInterface, int32 UpdateFlags) { check(false); return RegisterElementWithNavOctree(FNavigationElement::CreateFromNavRelevantInterface(ElementInterface), UpdateFlags); } FSetElementId FNavigationDataHandler::RegisterElementWithNavOctree(const TSharedRef& ElementRef, const int32 UpdateFlags) { SCOPE_CYCLE_COUNTER(STAT_Navigation_RegisterNavOctreeElement); FSetElementId SetId; const FNavigationElement& NavigationElement = ElementRef.Get(); if (OctreeController.IsValid() == false) { UE_LOG(LogNavOctree, VeryVerbose, TEXT("IGNORE(%hs) %s: octree not created yet"), __FUNCTION__, *NavigationElement.GetPathName()); return SetId; } if (OctreeController.IsNavigationOctreeLocked()) { UE_LOG(LogNavOctree, Log, TEXT("IGNORE(%hs) %s: navigation octree locked"), __FUNCTION__, *NavigationElement.GetPathName()); return SetId; } UE_LOG(LogNavOctree, Log, TEXT("REG %s"), *NavigationElement.GetPathName()); bool bCanAdd = false; if (const TWeakObjectPtr& NavigationParent = NavigationElement.GetNavigationParent(); !NavigationParent.IsExplicitlyNull()) { OctreeController.AddChild(FNavigationElementHandle(NavigationParent), ElementRef); bCanAdd = true; } else { bCanAdd = (OctreeController.HasElementNavOctreeId(NavigationElement.GetHandle()) == false); } if (bCanAdd) { FNavigationDirtyElement UpdateInfo(ElementRef, UE::NavigationHelper::Private::GetDirtyFlag(UpdateFlags, ENavigationDirtyFlag::None), DirtyAreasController.bUseWorldPartitionedDynamicMode); SetId = OctreeController.PendingUpdates.FindId(NavigationElement.GetHandle()); if (SetId.IsValidId()) { // make sure this request stays, in case it has been invalidated already and keep any dirty areas UpdateInfo.ExplicitAreasToDirty = OctreeController.PendingUpdates[SetId].ExplicitAreasToDirty; OctreeController.PendingUpdates[SetId] = UpdateInfo; } else { SetId = OctreeController.PendingUpdates.Add(UpdateInfo); } } return SetId; } void FNavigationDataHandler::AddElementToNavOctree(const FNavigationDirtyElement& DirtyElement) { check(OctreeController.IsValid()); LLM_SCOPE_BYTAG(NavigationOctree); // handle invalidated requests first if (DirtyElement.bInvalidRequest) { if (DirtyElement.bHasPrevData) { DirtyAreasController.AddArea(DirtyElement.PrevBounds, DirtyElement.PrevFlags, [&DirtyElement] { return DirtyElement.NavigationElement; }, &DirtyElement, "Addition to navoctree (invalid request)"); } return; } FNavigationOctreeElement OctreeElement(DirtyElement.NavigationElement); const FNavigationElement& NavigationElement = DirtyElement.NavigationElement.Get(); const TWeakObjectPtr ElementWeakUObject = NavigationElement.GetWeakUObject(); if (!ElementWeakUObject.IsExplicitlyNull()) { UE_VLOG_UELOG(ElementWeakUObject.Get(), LogNavOctree, Verbose, TEXT("Create FNavigationOctreeElement for %s"), *NavigationElement.GetPathName()); } // In WP dynamic mode, store if this is loaded data. if (DirtyAreasController.bUseWorldPartitionedDynamicMode) { OctreeElement.Data->bLoadedData = DirtyElement.bIsFromVisibilityChange || NavigationElement.IsFromLevelVisibilityChange(); } const FBox ElementBounds = NavigationElement.GetBounds(); if (const TWeakObjectPtr& NavigationParent = NavigationElement.GetNavigationParent(); !NavigationParent.IsExplicitlyNull()) { const FNavigationElementHandle ParentKey(NavigationParent); // check if parent node is waiting in queue const FSetElementId ParentRequestId = OctreeController.PendingUpdates.FindId(ParentKey); const FOctreeElementId2* ParentId = OctreeController.GetNavOctreeIdForElement(ParentKey); if (ParentRequestId.IsValidId() && ParentId == nullptr) { FNavigationDirtyElement& ParentDirtyElement = OctreeController.PendingUpdates[ParentRequestId]; AddElementToNavOctree(ParentDirtyElement); // mark as invalid so it won't be processed twice ParentDirtyElement.bInvalidRequest = true; } const FOctreeElementId2* ElementId = ParentId ? ParentId : OctreeController.GetNavOctreeIdForElement(ParentKey); if (ElementId && ensure(OctreeController.IsValidElement(*ElementId))) { UE_LOG(LogNavOctree, Log, TEXT("ADD %s to %s"), *NavigationElement.GetPathName(), *GetNameSafe(NavigationParent.Get())); OctreeController.NavOctree->AppendToNode(*ElementId, DirtyElement.NavigationElement, ElementBounds, OctreeElement); } else { UE_LOG(LogNavOctree, Warning, TEXT("Can't add node [%s] - parent [%s] not found in octree!"), *NavigationElement.GetPathName(), *GetNameSafe(NavigationParent.Get())); } } else { OctreeController.NavOctree->AddNode(ElementBounds, OctreeElement); UE_SUPPRESS(LogNavOctree, Verbose, { const FOctreeElementId2* ElementId = OctreeController.GetNavOctreeIdForElement(DirtyElement.NavigationElement->GetHandle()); UE_VLOG_UELOG(NavigationElement.GetWeakUObject().Get(), LogNavOctree, Log, TEXT("ADD %s - %s"), *NavigationElement.GetPathName(), ElementId ? *LexToString(*ElementId) : TEXT("No element")); }); } // mark area occupied by given element as dirty except if explicitly set to skip this default behavior const ENavigationDirtyFlag DirtyFlag = DirtyElement.FlagsOverride != ENavigationDirtyFlag::None ? DirtyElement.FlagsOverride : OctreeElement.Data->GetDirtyFlag(); if (OctreeElement.Data->bShouldSkipDirtyAreaOnAddOrRemove) { if (DirtyElement.ExplicitAreasToDirty.Num() > 0) { DirtyAreasController.AddAreas( DirtyElement.ExplicitAreasToDirty, DirtyFlag, [ElementOwner = DirtyElement.NavigationElement] { return ElementOwner; }, &DirtyElement, "Addition to navoctree"); } } else if (!OctreeElement.IsEmpty()) { DirtyAreasController.AddArea( OctreeElement.Bounds.GetBox(), DirtyFlag, [ElementOwner = DirtyElement.NavigationElement] { return ElementOwner; }, &DirtyElement, "Addition to navoctree"); } } // Deprecated bool FNavigationDataHandler::UnregisterNavOctreeElement(UObject& ElementOwner, INavRelevantInterface& ElementInterface, int32 UpdateFlags) { return UnregisterElementWithNavOctree(FNavigationElement::CreateFromNavRelevantInterface(ElementInterface), UpdateFlags); } bool FNavigationDataHandler::UnregisterElementWithNavOctree(const TSharedRef& ElementRef, const int32 UpdateFlags) { SCOPE_CYCLE_COUNTER(STAT_Navigation_UnregisterNavOctreeElement); const FNavigationElement& NavRelevantElement = ElementRef.Get(); if (OctreeController.IsValid() == false) { UE_LOG(LogNavOctree, VeryVerbose, TEXT("IGNORE(%hs) %s: octree not created yet"), __FUNCTION__, *NavRelevantElement.GetPathName()); return false; } if (OctreeController.IsNavigationOctreeLocked()) { UE_LOG(LogNavOctree, Log, TEXT("IGNORE(%hs) %s: octree locked"), __FUNCTION__, *ElementRef.Get().GetPathName()); return false; } const FNavigationElementHandle NavRelevantElementHandle = NavRelevantElement.GetHandle(); const FOctreeElementId2* OctreeElementId = OctreeController.GetNavOctreeIdForElement(NavRelevantElementHandle); UE_VLOG_UELOG(NavRelevantElement.GetWeakUObject().Get(), LogNavOctree, Log, TEXT("UNREG %s %s"), *NavRelevantElement.GetPathName(), OctreeElementId ? *FString::Printf(TEXT("[exists %s]"), *LexToString(*OctreeElementId)) : TEXT("[doesn\'t exist]")); bool bUnregistered = false; if (OctreeElementId != nullptr) { RemoveFromNavOctree(*OctreeElementId, UpdateFlags); bUnregistered = true; } else if (const bool bCanRemoveChildNode = (UpdateFlags & FNavigationOctreeController::OctreeUpdate_ParentChain) == 0) { if (const TWeakObjectPtr& NavigationParent = NavRelevantElement.GetNavigationParent(); !NavigationParent.IsExplicitlyNull()) { // if node has navigation parent (= doesn't exist in octree on its own) // and it's not part of parent chain update // remove it from map and force update on parent to rebuild octree element const FNavigationElementHandle ParentKey(NavigationParent); OctreeController.RemoveChild(ParentKey, ElementRef); if (const FNavigationRelevantData* NavigationData = OctreeController.GetDataForElement(ParentKey)) { UpdateNavOctreeParentChain(NavigationData->SourceElement); } } } // mark pending update as invalid, it will be dirtied according to currently active settings if (const bool bCanInvalidateQueue = (UpdateFlags & FNavigationOctreeController::OctreeUpdate_Refresh) == 0) { const FSetElementId RequestId = OctreeController.PendingUpdates.FindId(NavRelevantElementHandle); if (RequestId.IsValidId()) { FNavigationDirtyElement& DirtyElement = OctreeController.PendingUpdates[RequestId]; // Only consider as unregistered when pending update was not already invalidated since return value must indicate // that ElementOwner was fully added or about to be added (valid pending update). bUnregistered |= (DirtyElement.bInvalidRequest == false); DirtyElement.bInvalidRequest = true; } } return bUnregistered; } // Deprecated void FNavigationDataHandler::UpdateNavOctreeElement(UObject& ElementOwner, INavRelevantInterface& ElementInterface, int32 UpdateFlags) { UpdateNavOctreeElement(FNavigationElementHandle(&ElementOwner), FNavigationElement::CreateFromNavRelevantInterface(ElementInterface), UpdateFlags); } void FNavigationDataHandler::UpdateNavOctreeElement(FNavigationElementHandle ElementHandle, const TSharedRef& UpdatedElement, int32 UpdateFlags) { INC_DWORD_STAT(STAT_Navigation_UpdateNavOctree); if (OctreeController.IsValid() == false) { UE_LOG(LogNavOctree, VeryVerbose, TEXT("IGNORE(%hs) %s: octree not created yet"), __FUNCTION__, *UpdatedElement.Get().GetPathName()); return; } if (OctreeController.IsNavigationOctreeLocked()) { UE_LOG(LogNavOctree, Log, TEXT("IGNORE(%hs) %s: octree locked"), __FUNCTION__, *UpdatedElement.Get().GetPathName()); return; } // grab existing octree data FBox CurrentBounds; ENavigationDirtyFlag CurrentFlags; const bool bAlreadyExists = OctreeController.GetNavOctreeElementData(ElementHandle, CurrentFlags, CurrentBounds); // don't invalidate pending requests UpdateFlags |= FNavigationOctreeController::OctreeUpdate_Refresh; // Use local shared reference to make sure to keep element alive to register back // since unregistering might remove the only reference. TSharedRef LocalElementRef = UpdatedElement; // Always try to unregister, even if element owner doesn't exist in octree (parent nodes). // This is also why we need to provide the new element and not only the handle, so we can access the parent (expected to be always the same for an update). UnregisterElementWithNavOctree(LocalElementRef, UpdateFlags); const FSetElementId RequestId = RegisterElementWithNavOctree(LocalElementRef, UpdateFlags); // add original data to pending registration request // so it could be dirtied properly when system receive unregister request while actor is still queued if (RequestId.IsValidId()) { FNavigationDirtyElement& UpdateInfo = OctreeController.PendingUpdates[RequestId]; UpdateInfo.PrevFlags = CurrentFlags; if (UpdateInfo.PrevBounds.IsValid) { // If we have something stored already we want to // sum it up, since we care about the whole bounding // box of changes that potentially took place UpdateInfo.PrevBounds += CurrentBounds; } else { UpdateInfo.PrevBounds = CurrentBounds; } UpdateInfo.bHasPrevData = bAlreadyExists; } UpdateNavOctreeParentChain(UpdatedElement, /*bSkipElementOwnerUpdate=*/ true); } // Deprecated void FNavigationDataHandler::UpdateNavOctreeParentChain(UObject& ElementOwner, bool bSkipElementOwnerUpdate) { if (const INavRelevantInterface* NavRelevantInterface = Cast(&ElementOwner)) { UpdateNavOctreeParentChain(FNavigationElement::CreateFromNavRelevantInterface(*NavRelevantInterface), bSkipElementOwnerUpdate); } } void FNavigationDataHandler::UpdateNavOctreeParentChain(const TSharedRef& Element, const bool bSkipElementOwnerUpdate) { constexpr int32 UpdateFlags = FNavigationOctreeController::OctreeUpdate_ParentChain | FNavigationOctreeController::OctreeUpdate_Refresh; TArray> ChildNodes; OctreeController.GetChildren(Element->GetHandle(), ChildNodes); auto ElementOwnerUpdateFunc = [&]()->bool { bool bShouldRegisterChildren = true; if (bSkipElementOwnerUpdate == false) { // Use local shared reference to make sure to keep element alive to register back // since unregistering might remove the only reference. TSharedRef LocalElementRef = Element; // We don't want to register NavOctreeElement if owner was not already registered or queued // so we use Unregister/Register combo instead of UpdateNavOctreeElement if (UnregisterElementWithNavOctree(LocalElementRef, UpdateFlags)) { const FSetElementId NewId = RegisterElementWithNavOctree(LocalElementRef, UpdateFlags); bShouldRegisterChildren = NewId.IsValidId(); } else { bShouldRegisterChildren = false; } } return bShouldRegisterChildren; }; if (ChildNodes.Num() == 0) { // Last child was removed, only need to rebuild owner's NavOctreeElement ElementOwnerUpdateFunc(); return; } for (const TSharedRef& ChildNode : ChildNodes) { UnregisterElementWithNavOctree(ChildNode, UpdateFlags); } if (const bool bShouldRegisterChildren = ElementOwnerUpdateFunc()) { for (const TSharedRef& ChildNode : ChildNodes) { RegisterElementWithNavOctree(ChildNode, UpdateFlags); } } } // Deprecated bool FNavigationDataHandler::UpdateNavOctreeElementBounds(UObject& ElementOwner, const FBox& NewBounds, const TConstArrayView DirtyAreas) { return UpdateNavOctreeElementBounds(FNavigationElementHandle(&ElementOwner), NewBounds, DirtyAreas); } bool FNavigationDataHandler::UpdateNavOctreeElementBounds(const FNavigationElementHandle ElementHandle, const FBox& NewBounds, const TConstArrayView DirtyAreas) { const FOctreeElementId2* ElementId = OctreeController.GetNavOctreeIdForElement(ElementHandle); if (ElementId != nullptr && ensure(OctreeController.IsValidElement(*ElementId))) { OctreeController.NavOctree->UpdateNode(*ElementId, NewBounds); // Dirty areas if (DirtyAreas.Num() > 0) { // Refresh ElementId since object may be stored in a different node after updating bounds ElementId = OctreeController.GetNavOctreeIdForElement(ElementHandle); if (ElementId != nullptr && ensure(OctreeController.IsValidElement(*ElementId))) { const FNavigationOctreeElement& ElementData = OctreeController.NavOctree->GetElementById(*ElementId); DirtyAreasController.AddAreas( DirtyAreas, ElementData.Data->GetDirtyFlag(), [SourceElement = ElementData.Data->SourceElement] { return SourceElement; }, /*DirtyElement*/ nullptr, "Bounds change"); } } return true; } // Update bounds and to append dirty areas to a pending update since the element is not added yet. if (const FSetElementId PendingElementId = OctreeController.PendingUpdates.FindId(ElementHandle); PendingElementId.IsValidId()) { if (FNavigationDirtyElement& DirtyElement = OctreeController.PendingUpdates[PendingElementId]; !DirtyElement.bInvalidRequest) { const TSharedRef Updated = MakeShared(DirtyElement.NavigationElement.Get()); Updated->SetBounds(NewBounds); DirtyElement.NavigationElement = Updated; DirtyElement.ExplicitAreasToDirty.Append(DirtyAreas); return true; } } return false; } // Deprecated bool FNavigationDataHandler::UpdateNavOctreeElementBounds(UObject& Object, const FBox& NewBounds, const FBox& DirtyArea) { return UpdateNavOctreeElementBounds(FNavigationElementHandle(&Object), NewBounds, TConstArrayView{DirtyArea}); } void FNavigationDataHandler::FindElementsInNavOctree(const FBox& QueryBox, const FNavigationOctreeFilter& Filter, TArray& Elements) { if (OctreeController.IsValid() == false) { UE_LOG(LogNavOctree, Warning, TEXT("FNavigationDataHandler::FindElementsInNavOctree gets called while NavOctree is null")); return; } OctreeController.NavOctree->FindElementsWithBoundsTest(QueryBox, [&Elements, &Filter](const FNavigationOctreeElement& Element) { if (Element.IsMatchingFilter(Filter)) { Elements.Add(Element); } }); } // Deprecated bool FNavigationDataHandler::ReplaceAreaInOctreeData(const UObject& Object, TSubclassOf OldArea, TSubclassOf NewArea, bool bReplaceChildClasses) { return ReplaceAreaInOctreeData(FNavigationElementHandle(&Object), OldArea, NewArea, bReplaceChildClasses); } bool FNavigationDataHandler::ReplaceAreaInOctreeData( const FNavigationElementHandle Element, const TSubclassOf OldArea, const TSubclassOf NewArea, const bool bReplaceChildClasses) const { FNavigationRelevantData* Data = OctreeController.GetMutableDataForElement(Element); if (Data == nullptr || Data->HasModifiers() == false) { return false; } for (FAreaNavModifier& AreaModifier : Data->Modifiers.GetMutableAreas()) { if (AreaModifier.GetAreaClass() == OldArea || (bReplaceChildClasses && AreaModifier.GetAreaClass()->IsChildOf(OldArea))) { AreaModifier.SetAreaClass(NewArea); } } for (FSimpleLinkNavModifier& SimpleLink : Data->Modifiers.GetSimpleLinks()) { for (FNavigationLink& Link : SimpleLink.Links) { if (Link.GetAreaClass() == OldArea || (bReplaceChildClasses && Link.GetAreaClass()->IsChildOf(OldArea))) { Link.SetAreaClass(NewArea); } } for (FNavigationSegmentLink& Link : SimpleLink.SegmentLinks) { if (Link.GetAreaClass() == OldArea || (bReplaceChildClasses && Link.GetAreaClass()->IsChildOf(OldArea))) { Link.SetAreaClass(NewArea); } } } ensureMsgf(Data->Modifiers.GetCustomLinks().IsEmpty(), TEXT("Not implemented yet")); return true; } void FNavigationDataHandler::AddLevelCollisionToOctree(ULevel& Level) { #if WITH_RECAST if (OctreeController.IsValid() && OctreeController.NavOctree->GetNavGeometryStoringMode() == FNavigationOctree::StoreNavGeometry) { const FNavigationElementHandle ElementKey(&Level); const TArray* LevelGeom = Level.GetStaticNavigableGeometry(); const FOctreeElementId2* ElementId = OctreeController.GetNavOctreeIdForElement(ElementKey); if (!ElementId && LevelGeom && LevelGeom->Num() > 0) { TSharedRef NavigationElement = MakeShared(Level, INDEX_NONE); FNavigationOctreeElement BSPElem(NavigationElement); // In WP dynamic mode, store if this is loaded data. if (DirtyAreasController.bUseWorldPartitionedDynamicMode) { BSPElem.Data->bLoadedData = Level.HasVisibilityChangeRequestPending(); } FRecastGeometryExport::ExportVertexSoupGeometry(*LevelGeom, *BSPElem.Data); const FBox& Bounds = BSPElem.Data->Bounds; if (!Bounds.GetExtent().IsNearlyZero()) { OctreeController.NavOctree->AddNode(Bounds, BSPElem); DirtyAreasController.AddArea( Bounds, ENavigationDirtyFlag::All, [SourceElement = NavigationElement] { return SourceElement; }, /*DirtyElement*/ nullptr, "Add level"); UE_LOG(LogNavOctree, Log, TEXT("ADD %s"), *NavigationElement.Get().GetPathName()); } } } #endif// WITH_RECAST } void FNavigationDataHandler::RemoveLevelCollisionFromOctree(ULevel& Level) { if (OctreeController.IsValid()) { const FNavigationElementHandle NavigationElementHandle(&Level); if (const FOctreeElementId2* OctreeElementId = OctreeController.GetNavOctreeIdForElement(NavigationElementHandle)) { UE_LOG(LogNavOctree, Log, TEXT("UNREG %s %s"), *Level.GetPathName(), OctreeElementId ? TEXT("[exists]") : TEXT("")); RemoveFromNavOctree(*OctreeElementId, FNavigationOctreeController::OctreeUpdate_Geometry); } } } void FNavigationDataHandler::ProcessPendingOctreeUpdates() { QUICK_SCOPE_CYCLE_COUNTER(STAT_Navigation_ProcessPendingOctreeUpdates); if (OctreeController.NavOctree) { // AddElementToNavOctree (through some of its resulting function calls) modifies PendingUpdates so invalidates the iterators, // (via WaitUntilAsyncPropertyReleased() / UpdateComponentInNavOctree() / RegisterElementWithNavOctree()). This means we can't iterate // through this set in the normal way. Previously the code iterated through this which also left us open to other potential bugs // in that we may have tried to modify elements we had already processed. while (TSet::TIterator It = OctreeController.PendingUpdates.CreateIterator()) { FNavigationDirtyElement DirtyElement = *It; It.RemoveCurrent(); AddElementToNavOctree(DirtyElement); } } OctreeController.PendingUpdates.Empty(32); } void FNavigationDataHandler::DemandLazyDataGathering(FNavigationRelevantData& ElementData) { // Do the lazy gathering on the element OctreeController.NavOctree->DemandLazyDataGathering(ElementData); // Check if any child asked for some lazy gathering if (ElementData.IsPendingChildLazyModifiersGathering()) { TArray> ChildNodes; OctreeController.GetChildren(ElementData.SourceElement->GetHandle(), ChildNodes); for (const TSharedRef& ChildNode : ChildNodes) { OctreeController.NavOctree->DemandChildLazyDataGathering(ElementData, ChildNode.Get()); } ElementData.bPendingChildLazyModifiersGathering = false; } }