// Copyright Epic Games, Inc. All Rights Reserved. #include "MassSmartObjectProcessor.h" #include "MassCommandBuffer.h" #include "MassCommonTypes.h" #include "MassDebugger.h" #include "MassExecutionContext.h" #include "MassSignalSubsystem.h" #include "MassSmartObjectBehaviorDefinition.h" #include "MassSmartObjectFragments.h" #include "MassSmartObjectRequest.h" #include "MassSmartObjectSettings.h" #include "MassSmartObjectTypes.h" #include "Misc/ScopeExit.h" #include "SmartObjectOctree.h" #include "SmartObjectSubsystem.h" #include "SmartObjectZoneAnnotations.h" #include "VisualLogger/VisualLogger.h" #include "ZoneGraphAnnotationSubsystem.h" #include "ZoneGraphSubsystem.h" //----------------------------------------------------------------------// // UMassSmartObjectCandidatesFinderProcessor //----------------------------------------------------------------------// void UMassSmartObjectCandidatesFinderProcessor::ConfigureQueries(const TSharedRef& EntityManager) { WorldRequestQuery.AddRequirement(EMassFragmentAccess::ReadOnly); WorldRequestQuery.AddRequirement(EMassFragmentAccess::ReadWrite); WorldRequestQuery.AddTagRequirement(EMassFragmentPresence::None); WorldRequestQuery.AddSubsystemRequirement(EMassFragmentAccess::ReadOnly); WorldRequestQuery.RegisterWithProcessor(*this); LaneRequestQuery.AddRequirement(EMassFragmentAccess::ReadOnly); LaneRequestQuery.AddRequirement(EMassFragmentAccess::ReadWrite); LaneRequestQuery.AddTagRequirement(EMassFragmentPresence::None); LaneRequestQuery.AddSubsystemRequirement(EMassFragmentAccess::ReadOnly); LaneRequestQuery.AddSubsystemRequirement(EMassFragmentAccess::ReadOnly); LaneRequestQuery.RegisterWithProcessor(*this); ProcessorRequirements.AddSubsystemRequirement(EMassFragmentAccess::ReadWrite); ProcessorRequirements.AddSubsystemRequirement(EMassFragmentAccess::ReadOnly); } UMassSmartObjectCandidatesFinderProcessor::UMassSmartObjectCandidatesFinderProcessor() : WorldRequestQuery(*this) , LaneRequestQuery(*this) { // 1. Frame T Behavior create a request(deferred entity creation) // 2. Frame T+1: Processor execute the request might mark it as done(deferred add tag flushed at the end of the frame) // 3. Frame T+1: Behavior could cancel request(deferred destroy entity) // If the processor does not run before the behaviors, step 2 and 3 are flipped and it will crash while flushing the deferred commands ExecutionOrder.ExecuteBefore.Add(UE::Mass::ProcessorGroupNames::Behavior); } void UMassSmartObjectCandidatesFinderProcessor::Execute(FMassEntityManager& EntityManager, FMassExecutionContext& ExecutionContext) { UMassSignalSubsystem& SignalSubsystem = ExecutionContext.GetMutableSubsystemChecked(); const UZoneGraphAnnotationSubsystem& AnnotationSubsystem = ExecutionContext.GetSubsystemChecked(); // Create filter FSmartObjectRequestFilter Filter; Filter.BehaviorDefinitionClasses = { USmartObjectMassBehaviorDefinition::StaticClass() }; // Build list of request owner entities to send a completion signal TArray EntitiesToSignal; auto BeginRequestProcessing = [](const FMassEntityHandle Entity, FMassExecutionContext& Context, FMassSmartObjectRequestResultFragment& Result) { Context.Defer().AddTag(Entity); Result.Candidates.NumSlots = 0; }; auto EndRequestProcessing = [](const UObject* LogOwner, const FMassEntityHandle Entity, FMassSmartObjectRequestResultFragment& Result) { if (Result.Candidates.NumSlots > 0) { TArrayView View = MakeArrayView(Result.Candidates.Slots.GetData(), Result.Candidates.NumSlots); Algo::Sort(View, [](const FSmartObjectCandidateSlot& LHS, const FSmartObjectCandidateSlot& RHS) { return LHS.Cost < RHS.Cost; }); } Result.bProcessed = true; #if WITH_MASSGAMEPLAY_DEBUG UE_VLOG(LogOwner, LogSmartObject, VeryVerbose, TEXT("[%s] search completed: found %d"), *Entity.DebugGetDescription(), Result.Candidates.NumSlots); #endif // WITH_MASSGAMEPLAY_DEBUG }; // Process world location based requests WorldRequestQuery.ForEachEntityChunk(ExecutionContext, [this, &Filter, &EntitiesToSignal, &BeginRequestProcessing, &EndRequestProcessing](FMassExecutionContext& Context) { const USmartObjectSubsystem& SmartObjectSubsystem = Context.GetSubsystemChecked(); const int32 NumEntities = Context.GetNumEntities(); EntitiesToSignal.Reserve(EntitiesToSignal.Num() + NumEntities); const TConstArrayView RequestList = Context.GetFragmentView(); const TArrayView ResultList = Context.GetMutableFragmentView(); TArray QueryResults; TArray SortedCandidateSlots; for (FMassExecutionContext::FEntityIterator EntityIt = Context.CreateEntityIterator(); EntityIt; ++EntityIt) { const FMassSmartObjectWorldLocationRequestFragment& RequestFragment = RequestList[EntityIt]; FMassSmartObjectRequestResultFragment& Result = ResultList[EntityIt]; EntitiesToSignal.Add(RequestFragment.RequestingEntity); const FVector& SearchOrigin = RequestFragment.SearchOrigin; const FBox& SearchBounds = FBox::BuildAABB(SearchOrigin, FVector(SearchExtents)); const FMassEntityHandle Entity = Context.GetEntity(EntityIt); bool bDisplayDebug = false; FColor DebugColor(FColor::White); #if WITH_MASSGAMEPLAY_DEBUG bDisplayDebug = UE::Mass::Debug::IsDebuggingEntity(Entity, &DebugColor); #endif // WITH_MASSGAMEPLAY_DEBUG BeginRequestProcessing(Entity, Context, Result); ON_SCOPE_EXIT{ EndRequestProcessing(&SmartObjectSubsystem, Entity, Result); }; Filter.UserTags = RequestFragment.UserTags; Filter.ActivityRequirements = RequestFragment.ActivityRequirements; QueryResults.Reset(); SmartObjectSubsystem.FindSmartObjects(FSmartObjectRequest(SearchBounds, Filter), QueryResults); SortedCandidateSlots.Reset(QueryResults.Num()); for (const FSmartObjectRequestResult& QueryResult : QueryResults) { const FVector SlotLocation = SmartObjectSubsystem.GetSlotLocation(QueryResult.SlotHandle).GetValue(); SortedCandidateSlots.Emplace(QueryResult, UE_REAL_TO_FLOAT_CLAMPED_MAX(FVector::DistSquared(SearchOrigin, SlotLocation))); #if WITH_MASSGAMEPLAY_DEBUG if (bDisplayDebug) { constexpr uint16 DebugRadius = 10; UE_VLOG_LOCATION(&SmartObjectSubsystem, LogSmartObject, Display, SlotLocation, DebugRadius, DebugColor, TEXT("%s"), *LexToString(QueryResult.SmartObjectHandle)); UE_VLOG_SEGMENT(&SmartObjectSubsystem, LogSmartObject, Display, SearchOrigin, SlotLocation, DebugColor, TEXT("")); } #endif // WITH_MASSGAMEPLAY_DEBUG } SortedCandidateSlots.Sort([](const FSmartObjectCandidateSlot& First, const FSmartObjectCandidateSlot& Second){ return First.Cost < Second.Cost; }); Result.Candidates.NumSlots = IntCastChecked(FMath::Min(FMassSmartObjectCandidateSlots::MaxNumCandidates, (uint32)SortedCandidateSlots.Num())); for (int ResultIndex = 0; ResultIndex < Result.Candidates.NumSlots; ResultIndex++) { Result.Candidates.Slots[ResultIndex] = SortedCandidateSlots[ResultIndex]; } } }); // Process lane based requests const FZoneGraphTag SmartObjectTag = GetDefault()->SmartObjectTag; USmartObjectZoneAnnotations* Annotations = Cast(AnnotationSubsystem.GetFirstAnnotationForTag(SmartObjectTag)); LaneRequestQuery.ForEachEntityChunk(ExecutionContext, [&AnnotationSubsystem, Annotations, &Filter, SmartObjectTag, &EntitiesToSignal, &BeginRequestProcessing, &EndRequestProcessing](FMassExecutionContext& Context) { #if WITH_MASSGAMEPLAY_DEBUG const UZoneGraphSubsystem& ZoneGraphSubsystem = Context.GetSubsystemChecked(); #endif // WITH_MASSGAMEPLAY_DEBUG const USmartObjectSubsystem& SmartObjectSubsystem = Context.GetSubsystemChecked(); const int32 NumEntities = Context.GetNumEntities(); EntitiesToSignal.Reserve(EntitiesToSignal.Num() + NumEntities); TConstArrayView RequestList = Context.GetFragmentView(); TArrayView ResultList = Context.GetMutableFragmentView(); // Cache latest used data since request are most of the time on the same zone graph FZoneGraphDataHandle LastUsedDataHandle; const FSmartObjectAnnotationData* GraphData = nullptr; for (FMassExecutionContext::FEntityIterator EntityIt = Context.CreateEntityIterator(); EntityIt; ++EntityIt) { const FMassSmartObjectLaneLocationRequestFragment& RequestFragment = RequestList[EntityIt]; FMassSmartObjectRequestResultFragment& Result = ResultList[EntityIt]; EntitiesToSignal.Add(RequestFragment.RequestingEntity); const FZoneGraphCompactLaneLocation RequestLocation = RequestFragment.CompactLaneLocation; const FZoneGraphLaneHandle RequestLaneHandle = RequestLocation.LaneHandle; const FMassEntityHandle Entity = Context.GetEntity(EntityIt); bool bDisplayDebug = false; #if WITH_MASSGAMEPLAY_DEBUG FColor DebugColor(FColor::White); bDisplayDebug = UE::Mass::Debug::IsDebuggingEntity(Entity, &DebugColor); #endif // WITH_MASSGAMEPLAY_DEBUG BeginRequestProcessing(Entity, Context, Result); ON_SCOPE_EXIT{ EndRequestProcessing(&SmartObjectSubsystem, Entity, Result); }; if (!ensureMsgf(RequestLaneHandle.IsValid(), TEXT("Requesting smart objects using an invalid handle"))) { continue; } if (Annotations == nullptr) { UE_VLOG(&SmartObjectSubsystem, LogSmartObject, Warning, TEXT("%d lane location based requests failed since SmartObject annotations are not available"), NumEntities); return; } // Fetch smart object data associated to the current graph if different than last used one if (LastUsedDataHandle != RequestLaneHandle.DataHandle) { LastUsedDataHandle = RequestLaneHandle.DataHandle; GraphData = Annotations->GetAnnotationData(RequestLaneHandle.DataHandle); } if (GraphData == nullptr) { continue; } // Fetch current annotations for the specified lane and look for the smart object tag const FZoneGraphTagMask LaneMask = AnnotationSubsystem.GetAnnotationTags(RequestLaneHandle); if (!LaneMask.Contains(SmartObjectTag)) { continue; } const FSmartObjectLaneLocationIndices* SmartObjectList = GraphData->LaneToLaneLocationIndicesLookup.Find(RequestLaneHandle.Index); if (SmartObjectList == nullptr || !ensureMsgf(SmartObjectList->SmartObjectLaneLocationIndices.Num() > 0, TEXT("Lookup table should only contains lanes with one or more associated object(s)."))) { continue; } Filter.UserTags = RequestFragment.UserTags; Filter.ActivityRequirements = RequestFragment.ActivityRequirements; for (const int32 Index : SmartObjectList->SmartObjectLaneLocationIndices) { // Find entry point using FindChecked since all smart objects added to LaneToSmartObjects lookup table // were also added to the entry point lookup table check(GraphData->SmartObjectLaneLocations.IsValidIndex(Index)); const FSmartObjectLaneLocation& EntryPoint = GraphData->SmartObjectLaneLocations[Index]; const FSmartObjectHandle Handle = EntryPoint.ObjectHandle; float Cost = 0.f; if (ensureMsgf(EntryPoint.LaneIndex == RequestLocation.LaneHandle.Index, TEXT("Must be on same lane to be able to use distance along lane."))) { // Only consider object ahead const float DistAhead = EntryPoint.DistanceAlongLane - RequestLocation.DistanceAlongLane; if (DistAhead < 0) { continue; } Cost = DistAhead; } // Make sure that we can use a slot in that object (availability with supported definitions, etc.) TArray SlotHandles; SmartObjectSubsystem.FindSlots(Handle, Filter, SlotHandles, FConstStructView::Make(FSmartObjectMassEntityUserData(Entity))); if (SlotHandles.IsEmpty()) { continue; } for (FSmartObjectSlotHandle SlotHandle : SlotHandles) { Result.Candidates.Slots[Result.Candidates.NumSlots++] = FSmartObjectCandidateSlot(FSmartObjectRequestResult(Handle, SlotHandle), Cost); #if WITH_MASSGAMEPLAY_DEBUG if (bDisplayDebug) { FZoneGraphLaneLocation RequestLaneLocation, EntryPointLaneLocation; ZoneGraphSubsystem.CalculateLocationAlongLane(RequestLaneHandle, RequestLocation.DistanceAlongLane, RequestLaneLocation); ZoneGraphSubsystem.CalculateLocationAlongLane(RequestLaneHandle, EntryPoint.DistanceAlongLane, EntryPointLaneLocation); constexpr uint16 DebugRadius = 10; FVector SlotLocation = SmartObjectSubsystem.GetSlotLocation(SlotHandle).Get(EntryPointLaneLocation.Position); UE_VLOG_LOCATION(&SmartObjectSubsystem, LogSmartObject, Display, SlotLocation, DebugRadius, DebugColor, TEXT("%s"), *LexToString(SlotHandle)); UE_VLOG_SEGMENT(&SmartObjectSubsystem, LogSmartObject, Display, SlotLocation, EntryPointLaneLocation.Position, DebugColor, TEXT("")); UE_VLOG_SEGMENT(&SmartObjectSubsystem, LogSmartObject, Display, RequestLaneLocation.Position, EntryPointLaneLocation.Position, DebugColor, TEXT("")); } #endif // WITH_MASSGAMEPLAY_DEBUG if (Result.Candidates.NumSlots == FMassSmartObjectCandidateSlots::MaxNumCandidates) { break; } } if (Result.Candidates.NumSlots == FMassSmartObjectCandidateSlots::MaxNumCandidates) { break; } } } }); // Signal entities that their search results are ready if (EntitiesToSignal.Num()) { SignalSubsystem.SignalEntities(UE::Mass::Signals::SmartObjectCandidatesReady, EntitiesToSignal); } } //----------------------------------------------------------------------// // UMassSmartObjectTimedBehaviorProcessor //----------------------------------------------------------------------// void UMassSmartObjectTimedBehaviorProcessor::ConfigureQueries(const TSharedRef& EntityManager) { EntityQuery.AddRequirement(EMassFragmentAccess::ReadWrite); EntityQuery.AddRequirement(EMassFragmentAccess::ReadWrite); EntityQuery.AddSubsystemRequirement(EMassFragmentAccess::ReadOnly); ProcessorRequirements.AddSubsystemRequirement(EMassFragmentAccess::ReadWrite); } UMassSmartObjectTimedBehaviorProcessor::UMassSmartObjectTimedBehaviorProcessor() : EntityQuery(*this) { ExecutionOrder.ExecuteInGroup = UE::Mass::ProcessorGroupNames::SyncWorldToMass; } void UMassSmartObjectTimedBehaviorProcessor::Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context) { TArray ToRelease; QUICK_SCOPE_CYCLE_COUNTER(UMassProcessor_SmartObjectTestBehavior_Run); EntityQuery.ForEachEntityChunk(Context, [this, &ToRelease](FMassExecutionContext& Context) { const USmartObjectSubsystem& SmartObjectSubsystem = Context.GetSubsystemChecked(); const TArrayView UserList = Context.GetMutableFragmentView(); const TArrayView TimedBehaviorFragments = Context.GetMutableFragmentView(); for (FMassExecutionContext::FEntityIterator EntityIt = Context.CreateEntityIterator(); EntityIt; ++EntityIt) { FMassSmartObjectUserFragment& SOUser = UserList[EntityIt]; FMassSmartObjectTimedBehaviorFragment& TimedBehaviorFragment = TimedBehaviorFragments[EntityIt]; ensureMsgf(SOUser.InteractionStatus == EMassSmartObjectInteractionStatus::InProgress, TEXT("TimedBehavior fragment should only be present for in-progress interactions: %s"), *Context.GetEntity(EntityIt).DebugGetDescription()); const float DT = Context.GetDeltaTimeSeconds(); float& UseTime = TimedBehaviorFragment.UseTime; UseTime = FMath::Max(UseTime - DT, 0.0f); const bool bMustRelease = UseTime <= 0.f; #if WITH_MASSGAMEPLAY_DEBUG const FMassEntityHandle Entity = Context.GetEntity(EntityIt); FColor DebugColor(FColor::White); const bool bIsDebuggingEntity = UE::Mass::Debug::IsDebuggingEntity(Entity, &DebugColor); if (bIsDebuggingEntity) { UE_CVLOG(bMustRelease, &SmartObjectSubsystem, LogSmartObject, Log, TEXT("[%s] stops using [%s]"), *Entity.DebugGetDescription(), *LexToString(SOUser.InteractionHandle)); UE_CVLOG(!bMustRelease, &SmartObjectSubsystem, LogSmartObject, VeryVerbose, TEXT("[%s] using [%s]. Time left: %.1f"), *Entity.DebugGetDescription(), *LexToString(SOUser.InteractionHandle), UseTime); const TOptional Transform = SmartObjectSubsystem.GetSlotTransform(SOUser.InteractionHandle); if (Transform.IsSet()) { constexpr float Radius = 40.f; const FVector HalfHeightOffset(0.f, 0.f, 100.f); const FVector Pos = Transform.GetValue().GetLocation(); const FVector Dir = Transform.GetValue().GetRotation().GetForwardVector(); UE_VLOG_CYLINDER(&SmartObjectSubsystem, LogSmartObject, Display, Pos - HalfHeightOffset, Pos + HalfHeightOffset, Radius, DebugColor, TEXT("")); UE_VLOG_ARROW(&SmartObjectSubsystem, LogSmartObject, Display, Pos, Pos + Dir * 2.0f * Radius, DebugColor, TEXT("")); } } #endif // WITH_MASSGAMEPLAY_DEBUG if (bMustRelease) { SOUser.InteractionStatus = EMassSmartObjectInteractionStatus::BehaviorCompleted; ToRelease.Add(Context.GetEntity(EntityIt)); } } }); if (ToRelease.Num()) { UMassSignalSubsystem& SignalSubsystem = Context.GetMutableSubsystemChecked(); SignalSubsystem.SignalEntities(UE::Mass::Signals::SmartObjectInteractionDone, ToRelease); } } //----------------------------------------------------------------------// // UMassSmartObjectUserFragmentDeinitializer //----------------------------------------------------------------------// UMassSmartObjectUserFragmentDeinitializer::UMassSmartObjectUserFragmentDeinitializer() : EntityQuery(*this) { ObservedType = FMassSmartObjectUserFragment::StaticStruct(); Operation = EMassObservedOperation::Remove; ExecutionFlags = (int32)EProcessorExecutionFlags::AllNetModes; } void UMassSmartObjectUserFragmentDeinitializer::ConfigureQueries(const TSharedRef& EntityManager) { EntityQuery.AddRequirement(EMassFragmentAccess::ReadWrite); EntityQuery.AddSubsystemRequirement(EMassFragmentAccess::ReadWrite); } void UMassSmartObjectUserFragmentDeinitializer::Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context) { EntityQuery.ForEachEntityChunk(Context, [this](FMassExecutionContext& Context) { USmartObjectSubsystem& SmartObjectSubsystem = Context.GetMutableSubsystemChecked(); const TArrayView SmartObjectUserFragments = Context.GetMutableFragmentView(); for (FMassExecutionContext::FEntityIterator EntityIt = Context.CreateEntityIterator(); EntityIt; ++EntityIt) { SmartObjectSubsystem.UnregisterSlotInvalidationCallback(SmartObjectUserFragments[EntityIt].InteractionHandle); } }); }