// Copyright Epic Games, Inc. All Rights Reserved. #include "MassEntityManager.h" #include "MassProcessingTypes.h" #include "MassEntityTestTypes.h" #include "MassEntityTypes.h" #include "MassCommandBuffer.h" #include "MassExecutionContext.h" #include "MassObserverNotificationTypes.h" #define LOCTEXT_NAMESPACE "MassTest" UE_DISABLE_OPTIMIZATION_SHIP //----------------------------------------------------------------------// // tests //----------------------------------------------------------------------// namespace UE::Mass::Test::Observers { auto EntityIndexSorted = [](const FMassEntityHandle& A, const FMassEntityHandle& B) { return A.Index < B.Index; }; struct FTagBaseOperation : FEntityTestBase { using FTagStruct = FTestTag_A; TArray AffectedEntities; UMassTestProcessorBase* ObserverProcessor = nullptr; EMassObservedOperation OperationObserved = EMassObservedOperation::MAX; TArray EntitiesInt; TArray EntitiesIntsFloat; TArray ExpectedEntities; bool bCommandsFlushed = false; // @return signifies if the test can continue virtual bool PerformOperation() { return false; } virtual bool SetUp() override { if (FEntityTestBase::SetUp()) { ObserverProcessor = NewTestProcessor(EntityManager); ObserverProcessor->EntityQuery.AddRequirement(EMassFragmentAccess::ReadOnly); ObserverProcessor->EntityQuery.AddTagRequirement(EMassFragmentPresence::All); ObserverProcessor->ForEachEntityChunkExecutionFunction = [bCommandsFlushedPtr = &bCommandsFlushed, AffectedEntitiesPtr = &AffectedEntities](FMassExecutionContext& Context) { AffectedEntitiesPtr->Append(Context.GetEntities().GetData(), Context.GetEntities().Num()); Context.Defer().PushCommand([&bCommandsFlushedPtr](FMassEntityManager&) { // dummy command, here just to catch if commands issue by observers got executed at all *bCommandsFlushedPtr = true; }); }; return true; } return false; } virtual bool InstantTest() override { FMassObserverManager& ObserverManager = EntityManager->GetObserverManager(); ObserverManager.AddObserverInstance(*FTagStruct::StaticStruct(), OperationObserved, *ObserverProcessor); EntityManager->BatchCreateEntities(IntsArchetype, 3, EntitiesInt); EntityManager->BatchCreateEntities(FloatsIntsArchetype, 3, EntitiesIntsFloat); if (PerformOperation()) { EntityManager->FlushCommands(); AITEST_EQUAL(TEXT("The observer is expected to be run for predicted number of entities"), AffectedEntities.Num(), ExpectedEntities.Num()); AITEST_TRUE(TEXT("The commands issued by the observer are flushed"), bCommandsFlushed); ExpectedEntities.Sort(EntityIndexSorted); AffectedEntities.Sort(EntityIndexSorted); for (int i = 0; i < ExpectedEntities.Num(); ++i) { AITEST_EQUAL(TEXT("Expected and affected sets should be the same"), AffectedEntities[i], ExpectedEntities[i]); } } return true; } }; struct FSingleEntitySingleArchetypeAdd : FTagBaseOperation { FSingleEntitySingleArchetypeAdd() { OperationObserved = EMassObservedOperation::Add; } virtual bool PerformOperation() override { ExpectedEntities = { EntitiesInt[1] }; EntityManager->Defer().AddTag(EntitiesInt[1]); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FSingleEntitySingleArchetypeAdd, "System.Mass.Observer.Tag.SingleEntitySingleArchetypeAdd"); struct FSingleEntitySingleArchetypeRemove : FTagBaseOperation { FSingleEntitySingleArchetypeRemove() { OperationObserved = EMassObservedOperation::Remove; } virtual bool PerformOperation() override { ExpectedEntities = { EntitiesInt[1] }; EntityManager->Defer().AddTag(EntitiesInt[1]); EntityManager->FlushCommands(); // since we're only observing tag removal we don't expect AffectedEntities to contain any data at this point AITEST_EQUAL(TEXT("Tag addition is not being observed and is not expected to produce results yet"), AffectedEntities.Num(), 0); EntityManager->Defer().RemoveTag(EntitiesInt[1]); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FSingleEntitySingleArchetypeRemove, "System.Mass.Observer.Tag.SingleEntitySingleArchetypeRemove"); struct FSingleEntitySingleArchetypeDestroy : FTagBaseOperation { FSingleEntitySingleArchetypeDestroy() { OperationObserved = EMassObservedOperation::Remove; } virtual bool PerformOperation() override { ExpectedEntities = { EntitiesInt[1] }; EntityManager->Defer().AddTag(EntitiesInt[1]); EntityManager->FlushCommands(); // since we're only observing tag removal we don't expect AffectedEntities to contain any data at this point AITEST_EQUAL(TEXT("FTagStruct addition is not being observed and is not expected to produce results yet"), AffectedEntities.Num(), 0); EntityManager->Defer().DestroyEntity(EntitiesInt[1]); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FSingleEntitySingleArchetypeDestroy, "System.Mass.Observer.Tag.SingleEntitySingleArchetypeDestroy"); struct FMultipleArchetypeAdd : FTagBaseOperation { FMultipleArchetypeAdd() { OperationObserved = EMassObservedOperation::Add; } virtual bool PerformOperation() override { ExpectedEntities = { EntitiesInt[0], EntitiesInt[2], EntitiesIntsFloat[1] }; for (const FMassEntityHandle& ModifiedEntity : ExpectedEntities) { EntityManager->Defer().AddTag(ModifiedEntity); } return true; } }; IMPLEMENT_AI_INSTANT_TEST(FMultipleArchetypeAdd, "System.Mass.Observer.Tag.MultipleArchetypesAdd"); struct FMultipleArchetypeAdd_Sync : FTagBaseOperation { FMultipleArchetypeAdd_Sync() { OperationObserved = EMassObservedOperation::Add; } virtual bool PerformOperation() override { ExpectedEntities = { EntitiesInt[0], EntitiesInt[2], EntitiesIntsFloat[1] }; for (const FMassEntityHandle& ModifiedEntity : ExpectedEntities) { EntityManager->AddTagToEntity(ModifiedEntity, FTagStruct::StaticStruct()); } return true; } }; IMPLEMENT_AI_INSTANT_TEST(FMultipleArchetypeAdd_Sync, "System.Mass.Observer.Tag.MultipleArchetypesAdd_Sync"); struct FMultipleArchetypeRemove : FTagBaseOperation { FMultipleArchetypeRemove() { OperationObserved = EMassObservedOperation::Remove; } virtual bool PerformOperation() override { ExpectedEntities = { EntitiesInt[0], EntitiesInt[2], EntitiesIntsFloat[1] }; for (const FMassEntityHandle& ModifiedEntity : ExpectedEntities) { EntityManager->Defer().AddTag(ModifiedEntity); } EntityManager->FlushCommands(); // since we're only observing tag removal we don't expect AffectedEntities to contain any data at this point AITEST_EQUAL(TEXT("FTagStruct addition is not being observed and is not expected to produce results yet"), AffectedEntities.Num(), 0); for (const FMassEntityHandle& ModifiedEntity : ExpectedEntities) { EntityManager->Defer().RemoveTag(ModifiedEntity); } return true; } }; IMPLEMENT_AI_INSTANT_TEST(FMultipleArchetypeRemove, "System.Mass.Observer.Tag.MultipleArchetypesRemove"); struct FMultipleArchetypeRemove_Sync : FTagBaseOperation { FMultipleArchetypeRemove_Sync() { OperationObserved = EMassObservedOperation::Remove; } virtual bool PerformOperation() override { ExpectedEntities = { EntitiesInt[0], EntitiesInt[2], EntitiesIntsFloat[1] }; for (const FMassEntityHandle& ModifiedEntity : ExpectedEntities) { EntityManager->AddTagToEntity(ModifiedEntity, FTagStruct::StaticStruct()); } // since we're only observing tag removal we don't expect AffectedEntities to contain any data at this point AITEST_EQUAL(TEXT("FTagStruct addition is not being observed and is not expected to produce results yet"), AffectedEntities.Num(), 0); for (const FMassEntityHandle& ModifiedEntity : ExpectedEntities) { EntityManager->RemoveTagFromEntity(ModifiedEntity, FTagStruct::StaticStruct()); } return true; } }; IMPLEMENT_AI_INSTANT_TEST(FMultipleArchetypeRemove_Sync, "System.Mass.Observer.Tag.MultipleArchetypesRemove_Sync"); struct FMultipleArchetypeDestroy : FTagBaseOperation { FMultipleArchetypeDestroy() { OperationObserved = EMassObservedOperation::Remove; } virtual bool PerformOperation() override { ExpectedEntities = { EntitiesInt[0], EntitiesInt[2], EntitiesIntsFloat[1] }; for (const FMassEntityHandle& ModifiedEntity : ExpectedEntities) { EntityManager->Defer().AddTag(ModifiedEntity); } EntityManager->FlushCommands(); // since we're only observing tag removal we don't expect AffectedEntities to contain any data at this point AITEST_EQUAL(TEXT("Tag addition is not being observed and is not expected to produce results yet"), AffectedEntities.Num(), 0); for (const FMassEntityHandle& ModifiedEntity : ExpectedEntities) { EntityManager->Defer().DestroyEntity(ModifiedEntity); } return true; } }; IMPLEMENT_AI_INSTANT_TEST(FMultipleArchetypeDestroy, "System.Mass.Observer.Tag.MultipleArchetypesDestroy"); struct FForbidModifyOnDestroy : FTagBaseOperation { using Super = FTagBaseOperation; FForbidModifyOnDestroy() { OperationObserved = EMassObservedOperation::Remove; } virtual bool SetUp() override { if (Super::SetUp()) { ObserverProcessor->ForEachEntityChunkExecutionFunction = [bCommandsFlushedPtr = &bCommandsFlushed, AffectedEntitiesPtr = &AffectedEntities](FMassExecutionContext& Context) { AffectedEntitiesPtr->Append(Context.GetEntities().GetData(), Context.GetEntities().Num()); // try changing the input entities' composition. const bool bIsProcessing = Context.GetEntityManagerChecked().IsProcessing(); //for (int32 EntityIndex = 0; EntityIndex < Context.GetEntities().Num(); ++EntityIndex) for (const FMassEntityHandle EntityHandle : Context.GetEntities()) { //Context.Defer().AddTag(EntityHandle); //Context.GetEntityManagerChecked().AddTagToEntity(EntityHandle, FTestTag_B::StaticStruct()); } Context.Defer().PushCommand([&bCommandsFlushedPtr](FMassEntityManager&) { // dummy command, here just to catch if commands issue by observers got executed at all *bCommandsFlushedPtr = true; }); }; return true; } return false; } virtual bool PerformOperation() override { ExpectedEntities = { EntitiesInt[0], EntitiesInt[2], EntitiesIntsFloat[1] }; for (const FMassEntityHandle& ModifiedEntity : ExpectedEntities) { EntityManager->Defer().AddTag(ModifiedEntity); } EntityManager->FlushCommands(); // since we're only observing tag removal we don't expect AffectedEntities to contain any data at this point AITEST_EQUAL(TEXT("Tag addition is not being observed and is not expected to produce results yet"), AffectedEntities.Num(), 0); for (const FMassEntityHandle& ModifiedEntity : ExpectedEntities) { EntityManager->Defer().DestroyEntity(ModifiedEntity); } return true; } }; IMPLEMENT_AI_INSTANT_TEST(FForbidModifyOnDestroy, "System.Mass.Observer.ForbidModifyOnDestroy"); struct FMultipleArchetypeSwap : FTagBaseOperation { FMultipleArchetypeSwap() { OperationObserved = EMassObservedOperation::Remove; } virtual bool PerformOperation() override { ExpectedEntities = { EntitiesIntsFloat[1], EntitiesInt[0], EntitiesInt[2] }; for (const FMassEntityHandle& ModifiedEntity : ExpectedEntities) { EntityManager->Defer().AddTag(ModifiedEntity); } EntityManager->FlushCommands(); // since we're only observing tag removal we don't expect AffectedEntities to contain any data at this point AITEST_EQUAL(TEXT("Tag addition is not being observed and is not expected to produce results yet"), AffectedEntities.Num(), 0); for (const FMassEntityHandle& ModifiedEntity : ExpectedEntities) { EntityManager->Defer().SwapTags(ModifiedEntity); } return true; } }; IMPLEMENT_AI_INSTANT_TEST(FMultipleArchetypeSwap, "System.Mass.Observer.Tag.MultipleArchetypesSwap"); struct FEntityCreation_Individuals : FTagBaseOperation { FEntityCreation_Individuals() { OperationObserved = EMassObservedOperation::Add; } virtual bool InstantTest() override { constexpr int32 EntitiesToSpawnCount = 6; FMassObserverManager& ObserverManager = EntityManager->GetObserverManager(); ObserverManager.AddObserverInstance(*FTagStruct::StaticStruct(), OperationObserved, *ObserverProcessor); int32 ArrayMidPoint = 0; { TSharedRef CreationContext = EntityManager->BatchCreateEntities(IntsArchetype, EntitiesToSpawnCount, EntitiesInt); ArrayMidPoint = EntitiesInt.Num() / 2; for (int32 Index = 0; Index < ArrayMidPoint; ++Index) { EntityManager->AddTagToEntity(EntitiesInt[Index], FTagStruct::StaticStruct()); } AITEST_EQUAL(TEXT("The tag observer is not expected to run yet"), AffectedEntities.Num(), 0); } AITEST_EQUAL(TEXT("The tag observer is expected to run just after FEntityCreationContext's destruction"), AffectedEntities.Num(), ArrayMidPoint); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FEntityCreation_Individuals, "System.Mass.Observer.Create.TagInvididualEntities"); struct FEntityCreation_Batched : FTagBaseOperation { FEntityCreation_Batched() { OperationObserved = EMassObservedOperation::Add; } virtual bool InstantTest() override { constexpr int32 EntitiesToSpawnCount = 6; FMassObserverManager& ObserverManager = EntityManager->GetObserverManager(); ObserverManager.AddObserverInstance(*FTagStruct::StaticStruct(), OperationObserved, *ObserverProcessor); { TSharedRef CreationContext = EntityManager->BatchCreateEntities(IntsArchetype, EntitiesToSpawnCount, EntitiesInt); EntityManager->BatchChangeTagsForEntities(CreationContext->GetEntityCollections(*EntityManager.Get()), FMassTagBitSet(*FTagStruct::StaticStruct()), FMassTagBitSet()); AITEST_TRUE(TEXT("The tag observer is not expected to run yet"), AffectedEntities.Num() == 0); AITEST_FALSE(TEXT("CreationContext's entity collection should be invalidated at this moment"), CreationContext->DebugAreEntityCollectionsUpToDate()); EntityManager->BatchChangeTagsForEntities(CreationContext->GetEntityCollections(*EntityManager.Get()), FMassTagBitSet(*FTagStruct::StaticStruct()), FMassTagBitSet()); AITEST_TRUE(TEXT("The tag observer is still not expected to run"), AffectedEntities.Num() == 0); } AITEST_TRUE(TEXT("The tag observer is expected to run just after FEntityCreationContext's destruction"), AffectedEntities.Num() > 0); AITEST_EQUAL(TEXT("The tag observer is expected to process every entity just once"), AffectedEntities.Num(), EntitiesInt.Num()); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FEntityCreation_Batched, "System.Mass.Observer.Create.TagBatchedEntities"); //----------------------------------------------------------------------------- // fragments //----------------------------------------------------------------------------- struct FFragmentTestBase : FEntityTestBase { using FFragmentStruct = FTestFragment_Float; TArray AffectedEntities; UMassTestProcessorBase* ObserverProcessor = nullptr; EMassObservedOperation OperationObserved = EMassObservedOperation::MAX; TArray EntitiesFloats; TArray EntitiesInt; TArray EntitiesIntsFloat; TArray ExpectedEntities; bool bCommandsFlushed = false; // @return signifies if the test can continue virtual bool PerformOperation() { return false; } virtual bool SetUp() override { if (FEntityTestBase::SetUp()) { ObserverProcessor = NewTestProcessor(EntityManager); ObserverProcessor->EntityQuery.AddRequirement(FFragmentStruct::StaticStruct(), EMassFragmentAccess::ReadWrite); ObserverProcessor->ForEachEntityChunkExecutionFunction = [bCommandsFlushedPtr = &bCommandsFlushed, AffectedEntitiesPtr = &AffectedEntities](FMassExecutionContext& Context) { AffectedEntitiesPtr->Append(Context.GetEntities().GetData(), Context.GetEntities().Num()); Context.Defer().PushCommand([&bCommandsFlushedPtr](FMassEntityManager&) { // dummy command, here just to catch if commands issue by observers got executed at all *bCommandsFlushedPtr = true; }); }; return true; } return false; } virtual bool InstantTest() override { EntityManager->BatchCreateEntities(IntsArchetype, 3, EntitiesInt); EntityManager->BatchCreateEntities(FloatsIntsArchetype, 3, EntitiesIntsFloat); FMassObserverManager& ObserverManager = EntityManager->GetObserverManager(); ObserverManager.AddObserverInstance(*FFragmentStruct::StaticStruct(), OperationObserved, *ObserverProcessor); if (PerformOperation()) { EntityManager->FlushCommands(); AITEST_EQUAL(TEXT("The fragment observer is expected to be run for predicted number of entities"), AffectedEntities.Num(), ExpectedEntities.Num()); AITEST_TRUE(TEXT("The commands issued by the observer are flushed"), bCommandsFlushed); ExpectedEntities.Sort(EntityIndexSorted); AffectedEntities.Sort(EntityIndexSorted); for (int i = 0; i < ExpectedEntities.Num(); ++i) { AITEST_EQUAL(TEXT("Expected and affected sets should be the same"), AffectedEntities[i], ExpectedEntities[i]); } } return true; } }; struct FFragmentTest_SingleEntitySingleArchetypeAdd : FFragmentTestBase { FFragmentTest_SingleEntitySingleArchetypeAdd() { OperationObserved = EMassObservedOperation::Add; } virtual bool PerformOperation() override { ExpectedEntities = { EntitiesInt[1] }; EntityManager->Defer().AddFragment(EntitiesInt[1]); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FFragmentTest_SingleEntitySingleArchetypeAdd, "System.Mass.Observer.Fragment.SingleEntitySingleArchetypeAdd"); struct FFragmentTest_SingleEntitySingleArchetypeRemove : FFragmentTestBase { FFragmentTest_SingleEntitySingleArchetypeRemove() { OperationObserved = EMassObservedOperation::Remove; } virtual bool PerformOperation() override { ExpectedEntities = { EntitiesInt[1] }; EntityManager->Defer().AddFragment(EntitiesInt[1]); EntityManager->FlushCommands(); // since we're only observing Fragment removal we don't expect AffectedEntities to contain any data at this point AITEST_EQUAL(TEXT("Fragment addition is not being observed and is not expected to produce results yet"), AffectedEntities.Num(), 0); EntityManager->Defer().RemoveFragment(EntitiesInt[1]); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FFragmentTest_SingleEntitySingleArchetypeRemove, "System.Mass.Observer.Fragment.SingleEntitySingleArchetypeRemove"); struct FFragmentTest_SingleEntitySingleArchetypeDestroy : FFragmentTestBase { FFragmentTest_SingleEntitySingleArchetypeDestroy() { OperationObserved = EMassObservedOperation::Remove; } virtual bool PerformOperation() override { ExpectedEntities = { EntitiesInt[1] }; EntityManager->Defer().AddFragment(EntitiesInt[1]); EntityManager->FlushCommands(); // since we're only observing Fragment removal we don't expect AffectedEntities to contain any data at this point AITEST_EQUAL(TEXT("Fragment addition is not being observed and is not expected to produce results yet"), AffectedEntities.Num(), 0); EntityManager->Defer().DestroyEntity(EntitiesInt[1]); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FFragmentTest_SingleEntitySingleArchetypeDestroy, "System.Mass.Observer.Fragment.SingleEntitySingleArchetypeDestroy"); struct FFragmentTest_MultipleArchetypeAdd : FFragmentTestBase { FFragmentTest_MultipleArchetypeAdd() { OperationObserved = EMassObservedOperation::Add; } virtual bool PerformOperation() override { ExpectedEntities = { EntitiesInt[0], EntitiesInt[2], EntitiesInt[1] }; for (const FMassEntityHandle& ModifiedEntity : ExpectedEntities) { EntityManager->Defer().AddFragment(ModifiedEntity); } // also adding the fragment to the other archetype that already has the fragment. This should not yield any results for (const FMassEntityHandle& OtherEntity : EntitiesIntsFloat) { EntityManager->Defer().AddFragment(OtherEntity); } return true; } }; IMPLEMENT_AI_INSTANT_TEST(FFragmentTest_MultipleArchetypeAdd, "System.Mass.Observer.Fragment.MultipleArchetypesAdd"); struct FFragmentTest_MultipleArchetypeRemove : FFragmentTestBase { FFragmentTest_MultipleArchetypeRemove() { OperationObserved = EMassObservedOperation::Remove; } virtual bool PerformOperation() override { ExpectedEntities = { EntitiesInt[0], EntitiesInt[2], EntitiesIntsFloat[1] }; for (const FMassEntityHandle& ModifiedEntity : ExpectedEntities) { EntityManager->Defer().AddFragment(ModifiedEntity); } EntityManager->FlushCommands(); // since we're only observing Fragment removal we don't expect AffectedEntities to contain any data at this point AITEST_EQUAL(TEXT("Fragment addition is not being observed and is not expected to produce results yet"), AffectedEntities.Num(), 0); for (const FMassEntityHandle& ModifiedEntity : ExpectedEntities) { EntityManager->Defer().RemoveFragment(ModifiedEntity); } return true; } }; IMPLEMENT_AI_INSTANT_TEST(FFragmentTest_MultipleArchetypeRemove, "System.Mass.Observer.Fragment.MultipleArchetypesRemove"); struct FFragmentTest_MultipleArchetypeDestroy : FFragmentTestBase { FFragmentTest_MultipleArchetypeDestroy() { OperationObserved = EMassObservedOperation::Remove; } virtual bool PerformOperation() override { ExpectedEntities = { EntitiesInt[0], EntitiesInt[2], EntitiesIntsFloat[1] }; for (const FMassEntityHandle& ModifiedEntity : ExpectedEntities) { EntityManager->Defer().AddFragment(ModifiedEntity); } EntityManager->FlushCommands(); // since we're only observing Fragment removal we don't expect AffectedEntities to contain any data at this point AITEST_EQUAL(TEXT("Fragment addition is not being observed and is not expected to produce results yet"), AffectedEntities.Num(), 0); for (const FMassEntityHandle& ModifiedEntity : ExpectedEntities) { EntityManager->Defer().DestroyEntity(ModifiedEntity); } return true; } }; IMPLEMENT_AI_INSTANT_TEST(FFragmentTest_MultipleArchetypeDestroy, "System.Mass.Observer.Fragment.MultipleArchetypesDestroy"); struct FFragmentTest_EntityCreation_Individual : FFragmentTestBase { FFragmentTest_EntityCreation_Individual() { OperationObserved = EMassObservedOperation::Add; } virtual bool InstantTest() override { constexpr float TestValue = 123.456f; float ValueOnNotification = 0.f; ObserverProcessor->ForEachEntityChunkExecutionFunction = [&ValueOnNotification](FMassExecutionContext& Context) //-V1047 - This lambda is cleared before routine exit { const TConstArrayView Fragments = Context.GetFragmentView(); for (int32 EntityIndex = 0; EntityIndex < Context.GetNumEntities(); EntityIndex++) { ValueOnNotification = Fragments[EntityIndex].Value; }; }; FMassObserverManager& ObserverManager = EntityManager->GetObserverManager(); ObserverManager.AddObserverInstance(*FFragmentStruct::StaticStruct(), OperationObserved, *ObserverProcessor); TArray FragmentInstanceList = { FInstancedStruct::Make(FFragmentStruct(TestValue)) }; // BuildEntity { const FMassEntityHandle Entity= EntityManager->ReserveEntity(); EntityManager->BuildEntity(Entity, FragmentInstanceList); AITEST_EQUAL(TEXT("The fragment observer notified by BuildEntity is expected to be able to fetch the initial value"), ValueOnNotification, TestValue); EntityManager->DestroyEntity(Entity); } // CreateEntity { ValueOnNotification = 0.f; const FMassEntityHandle Entity = EntityManager->CreateEntity(FragmentInstanceList); AITEST_EQUAL(TEXT("The fragment observer notified by CreateEntity is expected to be able to fetch the initial value"), ValueOnNotification, TestValue); EntityManager->DestroyEntity(Entity); } ObserverProcessor->ForEachEntityChunkExecutionFunction = nullptr; return true; } }; IMPLEMENT_AI_INSTANT_TEST(FFragmentTest_EntityCreation_Individual, "System.Mass.Observer.Create.FragmentSingleEntity"); struct FFragmentTest_EntityCreation_Individuals : FFragmentTestBase { FFragmentTest_EntityCreation_Individuals() { OperationObserved = EMassObservedOperation::Add; } virtual bool InstantTest() override { constexpr int32 EntitiesToSpawnCount = 6; FMassObserverManager& ObserverManager = EntityManager->GetObserverManager(); ObserverManager.AddObserverInstance(*FFragmentStruct::StaticStruct(), OperationObserved, *ObserverProcessor); int32 ArrayMidPoint = 0; { TSharedRef CreationContext = EntityManager->BatchCreateEntities(IntsArchetype, EntitiesToSpawnCount, EntitiesInt); ArrayMidPoint = EntitiesInt.Num() / 2; for (int32 Index = 0; Index < ArrayMidPoint; ++Index) { EntityManager->AddFragmentToEntity(EntitiesInt[Index], FFragmentStruct::StaticStruct()); } AITEST_EQUAL(TEXT("The fragment observer is not expected to run yet"), AffectedEntities.Num(), 0); } AITEST_EQUAL(TEXT("The fragment observer is expected to run just after FEntityCreationContext's destruction"), AffectedEntities.Num(), ArrayMidPoint); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FFragmentTest_EntityCreation_Individuals, "System.Mass.Observer.Create.FragmentIndividualEntities"); #if WITH_MASSENTITY_DEBUG struct FObserverChangingComposition_Sync : FFragmentTestBase { FObserverChangingComposition_Sync() { OperationObserved = EMassObservedOperation::Add; } virtual bool InstantTest() override { constexpr int32 EntitiesToSpawn = 3; const FMassArchetypeHandle OriginalArchetype = FloatsArchetype; FMassObserverManager& ObserverManager = EntityManager->GetObserverManager(); ObserverManager.AddObserverInstance(*FFragmentStruct::StaticStruct(), OperationObserved, *ObserverProcessor); { AITEST_SCOPED_CHECK("Use asynchronous API instead", 1); ObserverProcessor->ForEachEntityChunkExecutionFunction = [EntityManager = EntityManager](FMassExecutionContext& Context) { EntityManager->AddFragmentToEntity(Context.GetEntity(0), FTestFragment_Int::StaticStruct()); }; EntityManager->BatchCreateEntities(OriginalArchetype, EntitiesToSpawn, EntitiesInt); AITEST_EQUAL("Number of entities in the original archetype, no moves expected", EntityManager->DebugGetArchetypeEntitiesCount(OriginalArchetype), EntitiesToSpawn); } { AITEST_SCOPED_CHECK("Use asynchronous API instead", 1); ObserverProcessor->ForEachEntityChunkExecutionFunction = [EntityManager = EntityManager, OriginalArchetype](FMassExecutionContext& Context) { FMassArchetypeEntityCollection EntityCollection(OriginalArchetype, Context.GetEntities(), FMassArchetypeEntityCollection::NoDuplicates); EntityManager->BatchChangeFragmentCompositionForEntities(MakeArrayView(&EntityCollection, 1) , FMassFragmentBitSet(*FTestFragment_Int::StaticStruct()), {}); }; EntityManager->BatchCreateEntities(OriginalArchetype, EntitiesToSpawn, EntitiesInt); AITEST_EQUAL("Number of entities in the original archetype, no moves expected", EntityManager->DebugGetArchetypeEntitiesCount(OriginalArchetype), EntitiesToSpawn * 2); } return true; } }; IMPLEMENT_AI_INSTANT_TEST(FObserverChangingComposition_Sync, "System.Mass.Observer.ChangingCompositionSync"); struct FObserverChangingComposition_Deferred : FFragmentTestBase { FObserverChangingComposition_Deferred() { OperationObserved = EMassObservedOperation::Add; } virtual bool InstantTest() override { constexpr int32 EntitiesToSpawn = 3; const FMassArchetypeHandle OriginalArchetype = FloatsArchetype; ObserverProcessor->ForEachEntityChunkExecutionFunction = [](FMassExecutionContext& Context) { Context.Defer().PushCommand>(Context.GetEntities()); }; FMassObserverManager& ObserverManager = EntityManager->GetObserverManager(); ObserverManager.AddObserverInstance(*FFragmentStruct::StaticStruct(), OperationObserved, *ObserverProcessor); EntityManager->BatchCreateEntities(OriginalArchetype, EntitiesToSpawn, EntitiesInt); AITEST_EQUAL("Number of entities in the original archetype", EntityManager->DebugGetArchetypeEntitiesCount(OriginalArchetype), 0); AITEST_EQUAL("Number of entities in the target archetype", EntityManager->DebugGetArchetypeEntitiesCount(FloatsIntsArchetype), EntitiesToSpawn); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FObserverChangingComposition_Deferred, "System.Mass.Observer.ChangingCompositionDeferred"); #endif // WITH_MASSENTITY_DEBUG /** * This test aims to verify expected behavior of observers when there's a creation context active, when composition-mutating * operations are affecting entities other than the ones being created. */ struct FModificationsWhileCreationContextActive : FTagBaseOperation { FModificationsWhileCreationContextActive() { OperationObserved = EMassObservedOperation::Add; } virtual bool InstantTest() override { constexpr int32 EntitiesToSpawnInFirstBatch = 3; constexpr int32 EntitiesToSpawnInSecondBatch = 5; EntityManager->BatchCreateEntities(IntsArchetype, EntitiesToSpawnInFirstBatch, EntitiesInt); FMassArchetypeEntityCollection InitialEntitiesCollection(IntsArchetype, EntitiesInt, FMassArchetypeEntityCollection::EDuplicatesHandling::NoDuplicates); FMassObserverManager& ObserverManager = EntityManager->GetObserverManager(); ObserverManager.AddObserverInstance(*FTagStruct::StaticStruct(), OperationObserved, *ObserverProcessor); { TSharedRef ObserversLock = EntityManager->GetOrMakeObserversLock(); { TSharedRef CreationContext = EntityManager->BatchCreateEntities(IntsArchetype, EntitiesToSpawnInSecondBatch, EntitiesInt); ensure(EntitiesInt.Num() == EntitiesToSpawnInFirstBatch + EntitiesToSpawnInSecondBatch); // note that the observers' behavior regarding the entities just created gets tested by FEntityCreation_Batched test above // we're testing only the behavior related to the previously created entities here } EntityManager->BatchChangeTagsForEntities(MakeArrayView(&InitialEntitiesCollection, 1) , FMassTagBitSet(*FTagStruct::StaticStruct()), FMassTagBitSet()); AITEST_TRUE(TEXT("The tag observer is not expected to run yet"), AffectedEntities.Num() == 0); } AITEST_TRUE(TEXT("The tag observer is expected to run just after FEntityCreationContext's destruction"), AffectedEntities.Num() > 0); AITEST_EQUAL(TEXT("The tag observer is expected to process only the original entities, that had a tag added to them"), AffectedEntities.Num(), EntitiesToSpawnInFirstBatch); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FModificationsWhileCreationContextActive, "System.Mass.Observer.Create.ModificationsToOtherEntities"); struct FCreationOperationOrder : FFragmentTestBase { int32 Counter = 0; FCreationOperationOrder() { OperationObserved = EMassObservedOperation::Add; } virtual void BuildScenario(TArray& PreExistingEntities, TArray& NewEntities) = 0; virtual bool InstantTest() override { TArray PreExistingEntities; TArray NewEntities; ObserverProcessor->ForEachEntityChunkExecutionFunction = [Counter = &this->Counter](FMassExecutionContext& Context) { for (auto EntityId : Context.CreateEntityIterator()) { Context.GetMutableFragmentView()[EntityId].Value = static_cast(++(*Counter)); } }; EntityManager->BatchCreateEntities(IntsArchetype, 2, PreExistingEntities); FMassObserverManager& ObserverManager = EntityManager->GetObserverManager(); ObserverManager.AddObserverInstance(*FFragmentStruct::StaticStruct(), OperationObserved, *ObserverProcessor); BuildScenario(PreExistingEntities, NewEntities); // the specific order of entities handled within a single creation context doesn't // need to match the assumed order float FirstBatchValues[] = { EntityManager->GetFragmentDataChecked(NewEntities[0]).Value , EntityManager->GetFragmentDataChecked(NewEntities[1]).Value }; AITEST_TRUE("First batch's values match", (FirstBatchValues[0] == 1.f && FirstBatchValues[1] == 2.f) || (FirstBatchValues[1] == 1.f && FirstBatchValues[0] == 2.f)); float PreExistingEntitiesValues[] = { EntityManager->GetFragmentDataChecked(PreExistingEntities[0]).Value , EntityManager->GetFragmentDataChecked(PreExistingEntities[1]).Value }; AITEST_EQUAL("First preexisting entity's value", PreExistingEntitiesValues[0], 3.f); AITEST_EQUAL("Second preexisting entity's value", PreExistingEntitiesValues[1], 6.f); float SecondBatchBatchValues[] = { EntityManager->GetFragmentDataChecked(NewEntities[2]).Value , EntityManager->GetFragmentDataChecked(NewEntities[3]).Value }; AITEST_TRUE("First batch's values match", (SecondBatchBatchValues[0] == 4.f && SecondBatchBatchValues[1] == 5.f) || (SecondBatchBatchValues[1] == 4.f && SecondBatchBatchValues[0] == 5.f)); return true; } }; struct FCreationOperationOrder_Batch : FCreationOperationOrder { virtual void BuildScenario(TArray& PreExistingEntities, TArray& NewEntities) override { TSharedRef ObserversLock = EntityManager->GetOrMakeObserversLock(); { // creating two separate entities, that should end up in the same creation context TSharedRef CreationContext = EntityManager->BatchCreateEntities(FloatsArchetype, 1, NewEntities); EntityManager->BatchCreateEntities(FloatsArchetype, 1, NewEntities); } { FMassArchetypeEntityCollection Collection(IntsArchetype, MakeArrayView(&PreExistingEntities[0], 1), FMassArchetypeEntityCollection::NoDuplicates); EntityManager->BatchChangeFragmentCompositionForEntities(MakeArrayView(&Collection, 1), FMassFragmentBitSet(*FTestFragment_Float::StaticStruct()), {}); } { // creating two separate entities, that should end up in the same creation context TSharedRef CreationContext = EntityManager->BatchCreateEntities(FloatsArchetype, 1, NewEntities); EntityManager->BatchCreateEntities(FloatsArchetype, 1, NewEntities); } { FMassArchetypeEntityCollection Collection(IntsArchetype, MakeArrayView(&PreExistingEntities[1], 1), FMassArchetypeEntityCollection::NoDuplicates); EntityManager->BatchChangeFragmentCompositionForEntities(MakeArrayView(&Collection, 1), FMassFragmentBitSet(*FTestFragment_Float::StaticStruct()), {}); } } }; IMPLEMENT_AI_INSTANT_TEST(FCreationOperationOrder_Batch, "System.Mass.Observer.Create.CreationOperationOrder.Batch"); struct FCreationOperationOrder_Individual : FCreationOperationOrder { virtual void BuildScenario(TArray& PreExistingEntities, TArray& NewEntities) override { TSharedRef ObserversLock = EntityManager->GetOrMakeObserversLock(); { // creating two separate entities, that should end up in the same creation context TSharedRef CreationContext = EntityManager->GetOrMakeCreationContext(); NewEntities.Add(EntityManager->CreateEntity(FloatsArchetype)); NewEntities.Add(EntityManager->CreateEntity(FloatsArchetype)); } EntityManager->AddFragmentToEntity(PreExistingEntities[0], FTestFragment_Float::StaticStruct()); { // creating two separate entities, that should end up in the same creation context TSharedRef CreationContext = EntityManager->GetOrMakeCreationContext(); NewEntities.Add(EntityManager->CreateEntity(FloatsArchetype)); NewEntities.Add(EntityManager->CreateEntity(FloatsArchetype)); } EntityManager->AddFragmentToEntity(PreExistingEntities[1], FTestFragment_Float::StaticStruct()); } }; IMPLEMENT_AI_INSTANT_TEST(FCreationOperationOrder_Individual, "System.Mass.Observer.Create.CreationOperationOrder.Individual"); // The scenario being tested: // 1. Create entities with Float fragment // 2. Add an unobserved tag A - results in created entities changing archetype // 3. Release the creation context - we expect Float observers to trigger struct FCreatedEntitiesUnobservedCompositionChange : FFragmentTestBase { FCreatedEntitiesUnobservedCompositionChange() { OperationObserved = EMassObservedOperation::Add; } virtual bool InstantTest() override { constexpr int32 EntitiesToSpawnCount = 6; FMassObserverManager& ObserverManager = EntityManager->GetObserverManager(); ObserverManager.AddObserverInstance(*FFragmentStruct::StaticStruct(), OperationObserved, *ObserverProcessor); { TSharedRef CreationContext = EntityManager->BatchCreateEntities(FloatsArchetype, EntitiesToSpawnCount, EntitiesFloats); // add unobserved tag EntityManager->BatchChangeTagsForEntities(CreationContext->GetEntityCollections(*EntityManager.Get()), FMassTagBitSet(*FTestTag_A::StaticStruct()), FMassTagBitSet()); AITEST_EQUAL(TEXT("The fragment observer is not expected to run yet"), AffectedEntities.Num(), 0); } AITEST_EQUAL(TEXT("The fragment observer is expected to run just after FEntityCreationContext's destruction"), AffectedEntities.Num(), EntitiesToSpawnCount); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FCreatedEntitiesUnobservedCompositionChange, "System.Mass.Observer.Create.UnobservedCompositionChange"); struct FMoveToAnotherArchetype_SingleEntity : FEntityTestBase { bool bTagAdded = false; bool bTagRemoved = false; bool bFloatAdded = false; bool bFloatRemoved = false; virtual bool SetUp() override { if (FEntityTestBase::SetUp() == false) { return false; } FMassObserverManager& ObserverManager = EntityManager->GetObserverManager(); auto CreateObserver = [this](const TFunction& StoreResultFunction) { UMassTestProcessorBase* ObserverProcessor = NewTestProcessor(EntityManager); ObserverProcessor->EntityQuery.AddRequirement(EMassFragmentAccess::ReadOnly); ObserverProcessor->ForEachEntityChunkExecutionFunction = StoreResultFunction; return ObserverProcessor; }; ObserverManager.AddObserverInstance(*FTestTag_A::StaticStruct(), EMassObservedOperation::Add , *CreateObserver([this](FMassExecutionContext& Context) { bTagAdded = true; } )); ObserverManager.AddObserverInstance(*FTestTag_A::StaticStruct(), EMassObservedOperation::Remove , *CreateObserver([this](FMassExecutionContext& Context) { bTagRemoved = true; } )); ObserverManager.AddObserverInstance(*FTestFragment_Float::StaticStruct(), EMassObservedOperation::Add , *CreateObserver([this](FMassExecutionContext& Context) { bFloatAdded = true; } )); ObserverManager.AddObserverInstance(*FTestFragment_Float::StaticStruct(), EMassObservedOperation::Remove , *CreateObserver([this](FMassExecutionContext& Context) { bFloatRemoved = true; } )); return true; } virtual bool InstantTest() override { const FMassEntityHandle EntityHandle = EntityManager->CreateEntity(IntsArchetype); // create target archetype const FMassArchetypeHandle TargetArchetypeHandle = EntityManager->CreateArchetype(IntsArchetype, { FTestTag_A::StaticStruct(), FTestFragment_Float::StaticStruct() }); EntityManager->MoveEntityToAnotherArchetype(EntityHandle, TargetArchetypeHandle); AITEST_TRUE("Tag addition observer has been executed", bTagAdded); AITEST_TRUE("Fragment addition observer has been executed", bFloatAdded); AITEST_FALSE("(NOT) Tag removal observer has been executed", bTagRemoved); AITEST_FALSE("(NOT) Fragment addition observer has been executed", bFloatRemoved); // moving back to the original archetype will remove the two added elements, and should trigger observers EntityManager->MoveEntityToAnotherArchetype(EntityHandle, IntsArchetype); AITEST_TRUE("Tag removal observer has been executed", bTagRemoved); AITEST_TRUE("Fragment addition observer has been executed", bFloatRemoved); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FMoveToAnotherArchetype_SingleEntity, "System.Mass.Observer.MoveToAnotherArchetype"); // //struct FRecursiveObserver : FEntityTestBase //{ // bool bTagAdded = false; // bool bTagRemoved = false; // // virtual bool SetUp() override // { // if (FEntityTestBase::SetUp() == false) // { // return false; // } // // FMassObserverManager& ObserverManager = EntityManager->GetObserverManager(); // // auto CreateObserver = [this](const TFunction& StoreResultFunction) // { // UMassTestProcessorBase* ObserverProcessor = NewTestProcessor(EntityManager); // ObserverProcessor->EntityQuery.AddRequirement(EMassFragmentAccess::ReadOnly); // ObserverProcessor->ForEachEntityChunkExecutionFunction = StoreResultFunction; // return ObserverProcessor; // }; // // ObserverManager.AddObserverInstance(*FTestTag_A::StaticStruct(), EMassObservedOperation::Add // , *CreateObserver([this](FMassExecutionContext& Context) // { // bTagAdded = true; // } // )); // // ObserverManager.AddObserverInstance(*FTestTag_A::StaticStruct(), EMassObservedOperation::Remove // , *CreateObserver([this](FMassExecutionContext& Context) // { // bTagRemoved = true; // } // )); // // return true; // } // // virtual bool InstantTest() override // { // const FMassEntityHandle EntityHandle = EntityManager->CreateEntity(IntsArchetype); // // // create target archetype // const FMassArchetypeHandle TargetArchetypeHandle = EntityManager->CreateArchetype(IntsArchetype, { FTestTag_A::StaticStruct(), FTestFragment_Float::StaticStruct() }); // // EntityManager->MoveEntityToAnotherArchetype(EntityHandle, TargetArchetypeHandle); // AITEST_TRUE("Tag addition observer has been executed", bTagAdded); // AITEST_TRUE("Fragment addition observer has been executed", bFloatAdded); // AITEST_FALSE("(NOT) Tag removal observer has been executed", bTagRemoved); // AITEST_FALSE("(NOT) Fragment addition observer has been executed", bFloatRemoved); // // // moving back to the original archetype will remove the two added elements, and should trigger observers // EntityManager->MoveEntityToAnotherArchetype(EntityHandle, IntsArchetype); // AITEST_TRUE("Tag removal observer has been executed", bTagRemoved); // AITEST_TRUE("Fragment addition observer has been executed", bFloatRemoved); // // return true; // } //}; //IMPLEMENT_AI_INSTANT_TEST(FMoveToAnotherArchetype_SingleEntity, "System.Mass.Observer.Recursive"); } // UE::Mass::Test::Observers UE_ENABLE_OPTIMIZATION_SHIP #undef LOCTEXT_NAMESPACE