// Copyright Epic Games, Inc. All Rights Reserved. #include "MassEntityBuilder.h" #include "MassCommandBuffer.h" #include "MassCommands.h" #include "MassEntityManager.h" #include "VisualLogger/VisualLogger.h" #if WITH_MASSENTITY_DEBUG #include "HAL/IConsoleManager.h" #endif // WITH_MASSENTITY_DEBUG namespace UE::Mass { #if WITH_MASSENTITY_DEBUG namespace Debug { bool bValidateEntityBuilderMakeInput = true; namespace { FAutoConsoleVariableRef AnonymousCVars[] = { {TEXT("mass.debug.ValidateEntityBuilderMakeInput"), bValidateEntityBuilderMakeInput , TEXT("When set, every call to FEntityBuilder::Make will verify if the struct values provided match declared entity composition.") , ECVF_Cheat} }; } } #endif // WITH_MASSENTITY_DEBUG namespace Private { struct FEntityBuilderHelper { template static void AppendFromEntity(FEntityBuilder& Builder, const FMassEntityHandle SourceEntityHandle, const FMassArchetypeCompositionDescriptor& ArchetypeComposition) { const auto& SourceContainer = ArchetypeComposition.GetContainer(); TArray& ElementInstanceContainer = Builder.GetInstancedStructContainerInternal(); // remove all the existing entries that match SourceContainer, and then just copy. for (auto Iterator = SourceContainer.GetIndexIterator(); Iterator; ++Iterator) { // @todo we could use an iterator that can fetch the type by simply calling Iterator.GetType() TNotNull ElementType = SourceContainer.GetTypeAtIndex(*Iterator); const int32 FoundIndex = ElementInstanceContainer.IndexOfByPredicate([&ElementType](const FInstancedStruct& ExistingElement) { return ExistingElement.GetScriptStruct() == ElementType; }); if (FoundIndex != INDEX_NONE) { ElementInstanceContainer.RemoveAtSwap(FoundIndex, EAllowShrinking::No); } } CopyFromEntity(Builder, SourceEntityHandle, ArchetypeComposition); } template static void CopyFromEntity(FEntityBuilder& Builder, const FMassEntityHandle SourceEntityHandle, const FMassArchetypeCompositionDescriptor& ArchetypeComposition) { const auto& SourceContainer = ArchetypeComposition.GetContainer(); TArray& ElementInstanceContainer = Builder.GetInstancedStructContainerInternal(); ElementInstanceContainer.Reserve(ElementInstanceContainer.Num() + SourceContainer.CountStoredTypes()); for (auto Iterator = SourceContainer.GetIndexIterator(); Iterator; ++Iterator) { // @todo we could use an iterator that can fetch the type by simply calling Iterator.GetType() TNotNull Type = SourceContainer.GetTypeAtIndex(*Iterator); FConstStructView SourceElementView = Builder.EntityManager->GetElementDataStruct(SourceEntityHandle, Type); // this happening is practically impossible, so testing only in debug checkSlow(SourceElementView.IsValid()); ElementInstanceContainer.Emplace(SourceElementView); } } }; #if WITH_MASSENTITY_DEBUG template bool CheckStructContainer(TConstArrayView Container, const TBitset& Bitset, const UObject* LogOwner) { bool bIssuesFound = false; for (const TWrapper& Element : Container) { if (Mass::IsA(Element.GetScriptStruct())) { if (Bitset.Contains(*Element.GetScriptStruct()) == false) { bIssuesFound = true; UE_VLOG_UELOG(LogOwner, LogMass, Error, TEXT("%hs: input Composition doesn't contain %s"), __FUNCTION__, *GetNameSafe(Element.GetScriptStruct())); } } else { bIssuesFound = true; UE_VLOG_UELOG(LogOwner, LogMass, Error, TEXT("%hs: %s is not a valid %s type") , __FUNCTION__, *GetNameSafe(Element.GetScriptStruct()), *TElement::StaticStruct()->GetName()); } } return bIssuesFound; } bool ValidateMakeInput(const TSharedRef& InEntityManager, const FMassArchetypeCompositionDescriptor& Composition , TConstArrayView InitialFragmentValues, TConstArrayView ConstSharedFragments, TConstArrayView SharedFragments) { const UObject* LogOwner = InEntityManager->GetOwner(); bool bIssuesFound = CheckStructContainer(InitialFragmentValues, Composition.GetFragments(), LogOwner); bIssuesFound |= CheckStructContainer(ConstSharedFragments, Composition.GetConstSharedFragments(), LogOwner); bIssuesFound |= CheckStructContainer(SharedFragments, Composition.GetSharedFragments(), LogOwner); return !bIssuesFound; } #endif // WITH_MASSENTITY_DEBUG } //----------------------------------------------------------------------------- // FEntityBuilder //----------------------------------------------------------------------------- FEntityBuilder::FEntityBuilder(FMassEntityManager& InEntityManager) : EntityManager(InEntityManager.AsShared()) { } FEntityBuilder::FEntityBuilder(const TSharedRef& InEntityManager) : EntityManager(InEntityManager) { } FEntityBuilder::FEntityBuilder(const FEntityBuilder& Other) : EntityManager(Other.EntityManager) { *this = Other; } FEntityBuilder& FEntityBuilder::operator=(const FEntityBuilder& Other) { if (testableEnsureMsgf(Other.IsValid(), TEXT("Copying invalid entity builder instances is not supported"))) { // if we already have an EntityHandle reserved we might want to keep it - why reserve a handle again // soon, the reserved handle doesn't have an archetype associated with it? // We do need to release the handle if we're dealing with a different entity manager (unexpected in practice, but possible [for now]) if (EntityManager != Other.EntityManager) { ConditionallyReleaseEntityHandle(); EntityManager = Other.EntityManager; } // we also reset the handle if this builder has already committed its entity - the entity needs to // be forgotten by this builder, it's "out in the wild" now and should be safe from accidental destruction. else if (State == EState::Committed) { EntityHandle.Reset(); } Composition = Other.Composition; Fragments = Other.Fragments; SharedFragments = Other.SharedFragments; ConstSharedFragments = Other.ConstSharedFragments; State = Composition.IsEmpty() ? EState::Empty : EState::ReadyToCommit; } return *this; } FEntityBuilder& FEntityBuilder::operator=(FEntityBuilder&& Other) { if (testableEnsureMsgf(Other.IsValid(), TEXT("Copying invalid entity builder instances is not supported"))) { // if we already have an EntityHandle reserved we might want to keep it - why reserve a handle again // soon, the reserved handle doesn't have an archetype associated with it? // We do need to release the handle if we're dealing with a different entity manager (unexpected in practice, but possible [for now]) if (EntityManager != Other.EntityManager) { ConditionallyReleaseEntityHandle(); EntityManager = MoveTemp(Other.EntityManager); } Fragments = MoveTemp(Other.Fragments); SharedFragments = MoveTemp(Other.SharedFragments); ConstSharedFragments = MoveTemp(Other.ConstSharedFragments); // the main point of the elaborated logic below is to avoid needlessly releasing reserved entities. if (HasReservedEntityHandle()) { if (Other.HasReservedEntityHandle()) { ConditionallyReleaseEntityHandle(); EntityHandle = Other.EntityHandle; } State = (Other.State == EState::Committed) ? EState::ReadyToCommit // we have a reserved entity at hand, we can Commit again : Other.State; } else { // we just take everything as is EntityHandle = Other.EntityHandle; State = Other.State; } Other.EntityHandle.Reset(); Other.State = EState::Invalid; } return *this; } FEntityBuilder::~FEntityBuilder() { ConditionallyReleaseEntityHandle(); } FEntityBuilder FEntityBuilder::Make(const TSharedRef& InEntityManager, const FMassArchetypeCompositionDescriptor& Composition , TConstArrayView InitialFragmentValues, TConstArrayView ConstSharedFragments, TConstArrayView SharedFragments) { FEntityBuilder Builder(InEntityManager); #if WITH_MASSENTITY_DEBUG if (Debug::bValidateEntityBuilderMakeInput) { ensureMsgf(Private::ValidateMakeInput(InEntityManager, Composition, InitialFragmentValues, ConstSharedFragments, SharedFragments) , TEXT("%hs: failed input validation. See log for details."), __FUNCTION__); } #endif // WITH_MASSENTITY_DEBUG Builder.Composition = Composition; Builder.Fragments = InitialFragmentValues; Builder.SharedFragments = SharedFragments; Builder.ConstSharedFragments = ConstSharedFragments; return Builder; } FEntityBuilder FEntityBuilder::Make(const TSharedRef& InEntityManager , const FMassArchetypeCompositionDescriptor& Composition , TArray&& InitialFragmentValues , TArray&& ConstSharedFragments , TArray&& SharedFragments) { FEntityBuilder Builder(InEntityManager); #if WITH_MASSENTITY_DEBUG if (Debug::bValidateEntityBuilderMakeInput) { ensureMsgf(Private::ValidateMakeInput(InEntityManager, Composition, InitialFragmentValues, ConstSharedFragments, SharedFragments) , TEXT("%hs: failed input validation. See log for details."), __FUNCTION__); } #endif // WITH_MASSENTITY_DEBUG Builder.Composition = Composition; Builder.Fragments = InitialFragmentValues; Builder.SharedFragments.Append(Forward>(SharedFragments)); Builder.ConstSharedFragments.Append(Forward>(ConstSharedFragments)); return Builder; } FMassEntityHandle FEntityBuilder::Commit() { // @todo consider locking every builder instance to a single thread to prevent concurrent add/flush if (!testableEnsureMsgf(State != EState::Committed, TEXT("Trying to commit an already committed EntityBuilder. This request will be ignored."))) { return EntityHandle; } if (Composition.IsEmpty()) { UE_VLOG_UELOG(EntityManager->GetOwner(), LogMass, Warning, TEXT("%hs: Attempting to commit while no composition has been configured."), __FUNCTION__); UE_CVLOG_UELOG(EntityHandle.IsValid(), EntityManager->GetOwner(), LogMass, Error, TEXT("Failing to commit while the entity handle has already been reserved.")); return FMassEntityHandle(); } CacheEntityHandle(); CacheSharedFragmentValue(); CacheArchetypeHandle(); if (EntityManager->IsProcessing()) { // we need to issue commands in this case EntityManager->Defer().PushCommand( [ReservedEntityHandle = EntityHandle, SharedFragmentValues = CachedSharedFragmentValues , ArchetypeHandle = CachedArchetypeHandle, FragmentsCopy = Fragments , RelationsParams = RelationsParams](FMassEntityManager& Manager) { FEntityBuilder::CreateEntityImpl(Manager, ReservedEntityHandle, ArchetypeHandle , SharedFragmentValues, FragmentsCopy, RelationsParams); }); } else { // directly create the entity, right now CreateEntityImpl(*EntityManager, EntityHandle, CachedArchetypeHandle, CachedSharedFragmentValues, Fragments, RelationsParams); } State = EState::Committed; return EntityHandle; } void FEntityBuilder::CreateEntityImpl(FMassEntityManager& EntityManager, FMassEntityHandle EntityHandle, const FMassArchetypeHandle& ArchetypeHandle , const FMassArchetypeSharedFragmentValues& SharedFragmentValues, const TConstArrayView Fragments, TConstArrayView RelationsParams) { // creating creation context to block observers from triggering until the // values are set with SetEntityFragmentValues. The lock gets auto-released at the end of the scope // or persists, if anyone else has a shared ptr to it TSharedRef CreationContext = EntityManager.GetOrMakeCreationContext(); EntityManager.BuildEntity(EntityHandle, ArchetypeHandle, SharedFragmentValues); EntityManager.SetEntityFragmentValues(EntityHandle, Fragments); for (const FPendingRelationParams& Relation : RelationsParams) { switch (Relation.OtherEntityRole) { case ERelationRole::Subject: EntityManager.GetRelationManager().CreateRelationInstance(Relation.RelationTypeHandle, Relation.OtherEntity, EntityHandle); break; case ERelationRole::Object: EntityManager.GetRelationManager().CreateRelationInstance(Relation.RelationTypeHandle, EntityHandle, Relation.OtherEntity); break; default: ensureMsgf(false, TEXT("Unhandled value %s"), *LexToString(Relation.OtherEntityRole)); } } } FMassEntityHandle FEntityBuilder::CommitAndReprepare() { FMassEntityHandle CreatedEntity = Commit(); Reprepare(); return CreatedEntity; } void FEntityBuilder::Reprepare() { if (ensureMsgf(State == EState::Committed, TEXT("Expected to be called only on Committed builders"))) { EntityHandle.Reset(); State = EState::ReadyToCommit; } } void FEntityBuilder::Reset(const bool bReleaseEntityHandleIfReserved) { if (bReleaseEntityHandleIfReserved) { ConditionallyReleaseEntityHandle(); } if (State != EState::Empty) { InvalidateCachedData(); State = EState::Empty; Composition.Reset(); Fragments.Reset(); SharedFragments.Reset(); ConstSharedFragments.Reset(); RelationsParams.Reset(); } } bool FEntityBuilder::SetReservedEntityHandle(const FMassEntityHandle ReservedEntityHandle) { if (!ensureMsgf(ReservedEntityHandle.IsValid() && EntityManager->IsEntityReserved(ReservedEntityHandle), TEXT("Input ReservedEntityHandle is expected to be valid and represent a reserved entity"))) { return false; } if (EntityHandle.IsValid() && EntityManager->IsEntityReserved(EntityHandle)) { checkf(IsCommitted() == false, TEXT("We only expect to be here when the entity builder has not been `Committed` yet")); EntityManager->ReleaseReservedEntity(EntityHandle); } EntityHandle = ReservedEntityHandle; return true; } bool FEntityBuilder::AppendDataFromEntity(const FMassEntityHandle SourceEntityHandle) { if (!testableEnsureMsgf(EntityManager->IsEntityActive(SourceEntityHandle) , TEXT("%hs expecting a valid, built entity as input"), __FUNCTION__)) { return false; } if (State == EState::Empty) { // copying is significantly more efficient (no lookups for existing data) return CopyDataFromEntity(SourceEntityHandle); } InvalidateCachedData(); const FMassArchetypeHandle ArchetypeHandle = EntityManager->GetArchetypeForEntity(SourceEntityHandle); const FMassArchetypeCompositionDescriptor& ArchetypeComposition = EntityManager->GetArchetypeComposition(ArchetypeHandle); Private::FEntityBuilderHelper::AppendFromEntity(*this, SourceEntityHandle, ArchetypeComposition); Private::FEntityBuilderHelper::AppendFromEntity(*this, SourceEntityHandle, ArchetypeComposition); Private::FEntityBuilderHelper::AppendFromEntity(*this, SourceEntityHandle, ArchetypeComposition); Composition.Append(ArchetypeComposition); State = Composition.IsEmpty() ? EState::Empty : EState::ReadyToCommit; return true; } bool FEntityBuilder::CopyDataFromEntity(const FMassEntityHandle SourceEntityHandle) { if (!testableEnsureMsgf(EntityManager->IsEntityActive(SourceEntityHandle) , TEXT("%hs expecting a valid, built entity as input"), __FUNCTION__)) { return false; } Reset(/*bReleaseEntityHandleIfReserved=*/false); const FMassArchetypeHandle ArchetypeHandle = EntityManager->GetArchetypeForEntity(SourceEntityHandle); const FMassArchetypeCompositionDescriptor& ArchetypeComposition = EntityManager->GetArchetypeComposition(ArchetypeHandle); Private::FEntityBuilderHelper::CopyFromEntity(*this, SourceEntityHandle, ArchetypeComposition); Private::FEntityBuilderHelper::CopyFromEntity(*this, SourceEntityHandle, ArchetypeComposition); Private::FEntityBuilderHelper::CopyFromEntity(*this, SourceEntityHandle, ArchetypeComposition); Composition = ArchetypeComposition; State = Composition.IsEmpty() ? EState::Empty : EState::ReadyToCommit; return true; } FMassEntityHandle FEntityBuilder::GetEntityHandle() const { CacheEntityHandle(); return EntityHandle; } void FEntityBuilder::ConditionallyReleaseEntityHandle() { if ((EntityHandle.IsValid() == true) && (State != EState::Committed)) { EntityManager->ReleaseReservedEntity(EntityHandle); } EntityHandle.Reset(); } void FEntityBuilder::CacheEntityHandle() const { if (EntityHandle.IsValid() == false) { checkf(State != EState::Committed, TEXT("Reserving an entity while the builder has already committed. This should not happen. Indicates an error during builder copying from another instance.")) EntityHandle = EntityManager->ReserveEntity(); } } void FEntityBuilder::CacheArchetypeHandle() { if (CachedArchetypeHandle.IsValid() == false) { CachedArchetypeHandle = EntityManager->CreateArchetype(Composition, ArchetypeCreationParams); } } void FEntityBuilder::InvalidateCachedData() { CachedArchetypeHandle = {}; CachedSharedFragmentValues.Reset(); } FMassArchetypeHandle FEntityBuilder::GetArchetypeHandle() { CacheArchetypeHandle(); return CachedArchetypeHandle; } void FEntityBuilder::CacheSharedFragmentValue() { if (CachedSharedFragmentValues.IsEmpty()) { for (FInstancedStruct& SharedFragmentInstance : SharedFragments) { check(SharedFragmentInstance.IsValid()); const FSharedStruct& SharedStruct = EntityManager->GetOrCreateSharedFragment(*SharedFragmentInstance.GetScriptStruct(), SharedFragmentInstance.GetMemory()); CachedSharedFragmentValues.Add(SharedStruct); } for (FInstancedStruct& ConstSharedFragmentInstance : ConstSharedFragments) { check(ConstSharedFragmentInstance.IsValid()); const FConstSharedStruct& ConstSharedStruct = EntityManager->GetOrCreateConstSharedFragment(*ConstSharedFragmentInstance.GetScriptStruct(), ConstSharedFragmentInstance.GetMemory()); CachedSharedFragmentValues.Add(ConstSharedStruct); } CachedSharedFragmentValues.Sort(); } } template bool FEntityBuilder::HandleTypeInstance(const UScriptStruct* Type, TArray*& OutTargetContainer, bool& bOutAlreadyInComposition) { if (UE::Mass::IsA(Type)) { auto& BitSet = Composition.GetContainer(); const int32 TypeIndex = BitSet.GetTypeIndex(*Type); OutTargetContainer = &GetInstancedStructContainerInternal(); if (BitSet.IsBitSet(TypeIndex) == false) { BitSet.AddAtIndex(TypeIndex); } else { bOutAlreadyInComposition = true; } return true; } return false; } template FEntityBuilder& FEntityBuilder::AddInternal(TInstancedStruct&& ElementInstance) { if (const UScriptStruct* Type = ElementInstance.GetScriptStruct()) { TArray* TargetContainer = nullptr; bool bAlreadyInComposition = false; // the following chain will stop at the first successful HandleTypeInstance call. // This is by design, since we don't support there being any overlap between the types. HandleTypeInstance(Type, TargetContainer, bAlreadyInComposition) || HandleTypeInstance(Type, TargetContainer, bAlreadyInComposition) || HandleTypeInstance(Type, TargetContainer, bAlreadyInComposition); if (TargetContainer) { auto* FoundElement = bAlreadyInComposition ? TargetContainer->FindByPredicate(FStructInstanceFindingPredicate{Type}) : nullptr; // note that it's perfectly fine for FoundElement being null while bAlreadyInComposition == true. // This will happen if the element type has been added just as a type first (which only annotates the bitset) // and then an instance of the same type gets added - the bitset already contains the bit, but // the container doesn't hold an instance yet. if (FoundElement) { *FoundElement = Forward(ElementInstance); } else { TargetContainer->Add(Forward(ElementInstance)); } } } return *this; } FEntityBuilder& FEntityBuilder::Add(const FInstancedStruct& ElementInstance) { return AddInternal(ElementInstance); } FEntityBuilder& FEntityBuilder::Add(FInstancedStruct&& ElementInstance) { return AddInternal(ElementInstance); } FEntityBuilder& FEntityBuilder::Add(TNotNull ElementType) { if (UE::Mass::IsA(ElementType)) { Composition.GetTags().Add(*ElementType); } else if (UE::Mass::IsA(ElementType)) { Composition.GetFragments().Add(*ElementType); } else if (UE::Mass::IsA(ElementType)) { Composition.GetSharedFragments().Add(*ElementType); } else if (UE::Mass::IsA(ElementType)) { Composition.GetConstSharedFragments().Add(*ElementType); } else if (ensureMsgf(UE::Mass::IsA(ElementType) , TEXT("Unhandled element type %s"), *ElementType->GetName())) { Composition.GetChunkFragments().Add(*ElementType); } State = EState::ReadyToCommit; CachedArchetypeHandle = FMassArchetypeHandle(); return *this; } FEntityBuilder& FEntityBuilder::AddRelation(const FTypeHandle RelationTypeHandle, const FMassEntityHandle OtherEntity, const ERelationRole InputEntityRole) { RelationsParams.Add({RelationTypeHandle, OtherEntity, InputEntityRole}); return *this; } void FEntityBuilder::ForEachRelation(const TFunctionRef& Operator) { for (int32 ElementIndex = RelationsParams.Num() - 1; ElementIndex >= 0; --ElementIndex) { if (Operator(RelationsParams[ElementIndex]) == false) { RelationsParams.RemoveAt(ElementIndex, EAllowShrinking::No); } } } } // namespace UE::Mass