Files
UnrealEngine/Engine/Source/Runtime/MassEntity/Private/MassEntityBuilder.cpp
2025-05-18 13:04:45 +08:00

510 lines
18 KiB
C++

// 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<typename T>
static void AppendFromEntity(FEntityBuilder& Builder, const FMassEntityHandle SourceEntityHandle, const FMassArchetypeCompositionDescriptor& ArchetypeComposition)
{
const auto& SourceContainer = ArchetypeComposition.GetContainer<T>();
TArray<FInstancedStruct>& ElementInstanceContainer = Builder.GetInstancedStructContainerInternal<T>();
// 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<const UScriptStruct*> 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<T>(Builder, SourceEntityHandle, ArchetypeComposition);
}
template<typename T>
static void CopyFromEntity(FEntityBuilder& Builder, const FMassEntityHandle SourceEntityHandle, const FMassArchetypeCompositionDescriptor& ArchetypeComposition)
{
const auto& SourceContainer = ArchetypeComposition.GetContainer<T>();
TArray<FInstancedStruct>& ElementInstanceContainer = Builder.GetInstancedStructContainerInternal<T>();
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<const UScriptStruct*> Type = SourceContainer.GetTypeAtIndex(*Iterator);
FConstStructView SourceElementView = Builder.EntityManager->GetElementDataStruct<T>(SourceEntityHandle, Type);
// this happening is practically impossible, so testing only in debug
checkSlow(SourceElementView.IsValid());
ElementInstanceContainer.Emplace(SourceElementView);
}
}
};
#if WITH_MASSENTITY_DEBUG
template<typename TElement, typename TBitset, typename TWrapper>
bool CheckStructContainer(TConstArrayView<TWrapper> Container, const TBitset& Bitset, const UObject* LogOwner)
{
bool bIssuesFound = false;
for (const TWrapper& Element : Container)
{
if (Mass::IsA<TElement>(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<FMassEntityManager>& InEntityManager, const FMassArchetypeCompositionDescriptor& Composition
, TConstArrayView<FInstancedStruct> InitialFragmentValues, TConstArrayView<FConstSharedStruct> ConstSharedFragments, TConstArrayView<FSharedStruct> SharedFragments)
{
const UObject* LogOwner = InEntityManager->GetOwner();
bool bIssuesFound = CheckStructContainer<FMassFragment>(InitialFragmentValues, Composition.Fragments, LogOwner);
bIssuesFound |= CheckStructContainer<FMassConstSharedFragment>(ConstSharedFragments, Composition.ConstSharedFragments, LogOwner);
bIssuesFound |= CheckStructContainer<FMassSharedFragment>(SharedFragments, Composition.SharedFragments, LogOwner);
return !bIssuesFound;
}
#endif // WITH_MASSENTITY_DEBUG
}
//-----------------------------------------------------------------------------
// FEntityBuilder
//-----------------------------------------------------------------------------
FEntityBuilder::FEntityBuilder(FMassEntityManager& InEntityManager)
: EntityManager(InEntityManager.AsShared())
{
}
FEntityBuilder::FEntityBuilder(const TSharedRef<FMassEntityManager>& 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<FMassEntityManager>& InEntityManager, const FMassArchetypeCompositionDescriptor& Composition
, TConstArrayView<FInstancedStruct> InitialFragmentValues, TConstArrayView<FConstSharedStruct> ConstSharedFragments, TConstArrayView<FSharedStruct> 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<FMassEntityManager>& InEntityManager
, const FMassArchetypeCompositionDescriptor& Composition
, TArray<FInstancedStruct>&& InitialFragmentValues
, TArray<FConstSharedStruct>&& ConstSharedFragments
, TArray<FSharedStruct>&& 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<TArray<FSharedStruct>>(SharedFragments));
Builder.ConstSharedFragments.Append(Forward<TArray<FConstSharedStruct>>(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<FMassDeferredCreateCommand>(
[ReservedEntityHandle = EntityHandle, SharedFragmentValues = CachedSharedFragmentValues, ArchetypeHandle = CachedArchetypeHandle, FragmentsCopy = Fragments](FMassEntityManager& Manager)
{
Manager.BuildEntity(ReservedEntityHandle, ArchetypeHandle, SharedFragmentValues);
Manager.SetEntityFragmentValues(ReservedEntityHandle, FragmentsCopy);
});
}
else
{
// directly create the entity
TSharedRef<FMassEntityManager::FEntityCreationContext> CreationContext = EntityManager->GetOrMakeCreationContext();
EntityManager->BuildEntity(EntityHandle, CachedArchetypeHandle, CachedSharedFragmentValues);
EntityManager->SetEntityFragmentValues(EntityHandle, Fragments);
}
State = EState::Committed;
return EntityHandle;
}
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();
}
}
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<FMassFragment>(*this, SourceEntityHandle, ArchetypeComposition);
Private::FEntityBuilderHelper::AppendFromEntity<FMassSharedFragment>(*this, SourceEntityHandle, ArchetypeComposition);
Private::FEntityBuilderHelper::AppendFromEntity<FMassConstSharedFragment>(*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<FMassFragment>(*this, SourceEntityHandle, ArchetypeComposition);
Private::FEntityBuilderHelper::CopyFromEntity<FMassSharedFragment>(*this, SourceEntityHandle, ArchetypeComposition);
Private::FEntityBuilderHelper::CopyFromEntity<FMassConstSharedFragment>(*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<typename T>
FORCEINLINE FEntityBuilder& FEntityBuilder::AddInternal(T&& ElementInstance)
{
if (const UScriptStruct* Type = ElementInstance.GetScriptStruct())
{
if (Type->IsChildOf(FMassFragment::StaticStruct()))
{
Composition.Fragments.Add(*Type);
Fragments.Add(Forward<T>(ElementInstance));
}
else if (Type->IsChildOf(FMassSharedFragment::StaticStruct()))
{
Composition.SharedFragments.Add(*Type);
SharedFragments.Add(Forward<T>(ElementInstance));
}
else if (Type->IsChildOf(FMassConstSharedFragment::StaticStruct()))
{
Composition.ConstSharedFragments.Add(*Type);
ConstSharedFragments.Add(Forward<T>(ElementInstance));
}
}
return *this;
}
FEntityBuilder& FEntityBuilder::Add(const FInstancedStruct& ElementInstance)
{
return AddInternal(ElementInstance);
}
FEntityBuilder& FEntityBuilder::Add(FInstancedStruct&& ElementInstance)
{
return AddInternal(ElementInstance);
}
} // namespace UE::Mass