diff --git a/Code/Framework/AzCore/AzCore/Serialization/IdUtils.h b/Code/Framework/AzCore/AzCore/Serialization/IdUtils.h index a571379883..98959fe4bd 100644 --- a/Code/Framework/AzCore/AzCore/Serialization/IdUtils.h +++ b/Code/Framework/AzCore/AzCore/Serialization/IdUtils.h @@ -28,7 +28,13 @@ namespace AZ { namespace IdUtils { - template + /** + * \param AllowDuplicates - If true allows the same id to be registered multiple time, + with the newer value overwriting the stored value. If false, duplicates are not allowed and + the first stored value is kept.The default is false. + */ + + template struct Remapper { /** @@ -138,14 +144,18 @@ namespace AZ * \param context - The serialize context for enumerating the @classPtr elements */ template - static void GenerateNewIdsAndFixRefs(T* object, MapType& newIdMap, AZ::SerializeContext* context = nullptr) + static void GenerateNewIdsAndFixRefs( + T* object, MapType& newIdMap, AZ::SerializeContext* context = nullptr) { if (!context) { AZ::ComponentApplicationBus::BroadcastResult(context, &AZ::ComponentApplicationRequests::GetSerializeContext); if (!context) { - AZ_Error("Serialization", false, "No serialize context provided! Failed to get component application default serialize context! ComponentApp is not started or input serialize context should not be null!"); + AZ_Error( + "Serialization", false, + "No serialize context provided! Failed to get component application default serialize context! ComponentApp is " + "not started or input serialize context should not be null!"); return; } } @@ -156,8 +166,16 @@ namespace AZ { if (idGenerator) { - auto it = newIdMap.emplace(originalId, idGenerator()); - return it.first->second; + if constexpr(AllowDuplicates) + { + auto it = newIdMap.insert_or_assign(originalId, idGenerator()); + return it.first->second; + } + else + { + auto it = newIdMap.emplace(originalId, idGenerator()); + return it.first->second; + } } return originalId; } diff --git a/Code/Framework/AzCore/AzCore/Serialization/IdUtils.inl b/Code/Framework/AzCore/AzCore/Serialization/IdUtils.inl index 01617cf5aa..0dc87dfe18 100644 --- a/Code/Framework/AzCore/AzCore/Serialization/IdUtils.inl +++ b/Code/Framework/AzCore/AzCore/Serialization/IdUtils.inl @@ -30,8 +30,10 @@ namespace AZ bool m_isModifiedContainer; }; - template - unsigned int Remapper::RemapIds(void* classPtr, const AZ::Uuid& classUuid, const typename Remapper::IdMapper& mapper, AZ::SerializeContext* context, bool replaceId) + template + unsigned int Remapper::RemapIds( + void* classPtr, const AZ::Uuid& classUuid, const typename Remapper::IdMapper& mapper, + AZ::SerializeContext* context, bool replaceId) { if (!context) { @@ -152,16 +154,18 @@ namespace AZ return replaced; } - template - unsigned int Remapper::ReplaceIdsAndIdRefs(void* classPtr, const AZ::Uuid& classUuid, const IdMapper& mapper, AZ::SerializeContext* context /*= nullptr*/) + template + unsigned int Remapper::ReplaceIdsAndIdRefs(void* classPtr, const AZ::Uuid& classUuid, const IdMapper& mapper, AZ::SerializeContext* context /*= nullptr*/) { unsigned int replaced = RemapIds(classPtr, classUuid, mapper, context, true); replaced += RemapIds(classPtr, classUuid, mapper, context, false); return replaced; } - template - unsigned int Remapper::RemapIdsAndIdRefs(void* classPtr, const AZ::Uuid& classUuid, const typename Remapper::IdReplacer& mapper, AZ::SerializeContext* context) + template + unsigned int Remapper::RemapIdsAndIdRefs( + void* classPtr, const AZ::Uuid& classUuid, const typename Remapper::IdReplacer& mapper, + AZ::SerializeContext* context) { if (!context) { diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h index 5d7ce79641..2ad1db60a4 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h @@ -172,7 +172,7 @@ namespace AzFramework using ClaimEntitiesCallback = AZStd::function; using BarrierCallback = AZStd::function; - struct SpawnEntitiesOptionalArgs final + struct SpawnAllEntitiesOptionalArgs final { //! Callback that's called after instances of entities have been created, but before they're spawned into the world. This //! gives the opportunity to modify the entities if needed such as injecting additional components or modifying components. @@ -186,6 +186,25 @@ namespace AzFramework SpawnablePriority m_priority { SpawnablePriority_Default }; }; + struct SpawnEntitiesOptionalArgs final + { + //! Callback that's called after instances of entities have been created, but before they're spawned into the world. This + //! gives the opportunity to modify the entities if needed such as injecting additional components or modifying components. + EntityPreInsertionCallback m_preInsertionCallback; + //! Callback that's called when spawning entities has completed. This can be called from a different thread than the one that + //! made the function call. The returned list of entities contains all the newly created entities. + EntitySpawnCallback m_completionCallback; + //! The Serialize Context used to clone entities with. If this is not provided the global Serialize Contetx will be used. + AZ::SerializeContext* m_serializeContext{ nullptr }; + //! The priority at which this call will be executed. + SpawnablePriority m_priority{ SpawnablePriority_Default }; + //! Entity references are resolved by referring to the last entity spawned from a template entity in the spawnable. If this + //! is set to false entities from previous spawn calls are not taken into account. If set to true entity references may be + //! resolved to a previously spawned entity. A lookup table has to be constructed when true, which may negatively impact + //! performance, especially if a large number of entities are present on a ticket. + bool m_referencePreviouslySpawnedEntities{ false }; + }; + struct DespawnAllEntitiesOptionalArgs final { //! Callback that's called when spawning entities has completed. This can be called from a different thread than the one that @@ -250,7 +269,7 @@ namespace AzFramework //! Spawn instances of all entities in the spawnable. //! @param ticket Stores the results of the call. Use this ticket to spawn additional entities or to despawn them. //! @param optionalArgs Optional additional arguments, see SpawnAllEntitiesOptionalArgs - virtual void SpawnAllEntities(EntitySpawnTicket& ticket, SpawnEntitiesOptionalArgs optionalArgs = {}) = 0; + virtual void SpawnAllEntities(EntitySpawnTicket& ticket, SpawnAllEntitiesOptionalArgs optionalArgs = {}) = 0; //! Spawn instances of some entities in the spawnable. //! @param ticket Stores the results of the call. Use this ticket to spawn additional entities or to despawn them. //! @param priority The priority at which this call will be executed. diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp index c11ce94bb2..a482c1d4f9 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp @@ -50,7 +50,7 @@ namespace AzFramework } } - void SpawnableEntitiesManager::SpawnAllEntities(EntitySpawnTicket& ticket, SpawnEntitiesOptionalArgs optionalArgs) + void SpawnableEntitiesManager::SpawnAllEntities(EntitySpawnTicket& ticket, SpawnAllEntitiesOptionalArgs optionalArgs) { AZ_Assert(ticket.IsValid(), "Ticket provided to SpawnAllEntities hasn't been initialized."); @@ -75,6 +75,7 @@ namespace AzFramework optionalArgs.m_serializeContext == nullptr ? m_defaultSerializeContext : optionalArgs.m_serializeContext; queueEntry.m_completionCallback = AZStd::move(optionalArgs.m_completionCallback); queueEntry.m_preInsertionCallback = AZStd::move(optionalArgs.m_preInsertionCallback); + queueEntry.m_referencePreviouslySpawnedEntities = optionalArgs.m_referencePreviouslySpawnedEntities; QueueRequest(ticket, optionalArgs.m_priority, AZStd::move(queueEntry)); } @@ -256,21 +257,11 @@ namespace AzFramework } } - AZ::Entity* SpawnableEntitiesManager::SpawnSingleEntity(const AZ::Entity& entityTemplate, AZ::SerializeContext& serializeContext) - { - AZ::Entity* clone = serializeContext.CloneObject(&entityTemplate); - AZ_Assert(clone != nullptr, "Failed to clone spawnable entity."); - clone->SetId(AZ::Entity::MakeId()); - - GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, clone); - return clone; - } - AZ::Entity* SpawnableEntitiesManager::CloneSingleEntity(const AZ::Entity& entityTemplate, - EntityIdMap& templateToCloneEntityIdMap, AZ::SerializeContext& serializeContext) + EntityIdMap& templateToCloneMap, AZ::SerializeContext& serializeContext) { - return AZ::IdUtils::Remapper::CloneObjectAndGenerateNewIdsAndFixRefs( - &entityTemplate, templateToCloneEntityIdMap, &serializeContext); + return AZ::IdUtils::Remapper::CloneObjectAndGenerateNewIdsAndFixRefs( + &entityTemplate, templateToCloneMap, &serializeContext); } bool SpawnableEntitiesManager::ProcessRequest(SpawnAllEntitiesCommand& request) @@ -297,13 +288,9 @@ namespace AzFramework spawnedEntityIndices.reserve(spawnedEntityIndices.size() + entitiesToSpawnSize); templateToCloneEntityIdMap.reserve(entitiesToSpawnSize); - // Mark all indices as spawned for (size_t i = 0; i < entitiesToSpawnSize; ++i) { - const AZ::Entity& entityTemplate = *entitiesToSpawn[i]; - - AZ::Entity* clone = CloneSingleEntity(entityTemplate, templateToCloneEntityIdMap, *request.m_serializeContext); - + AZ::Entity* clone = CloneSingleEntity(*entitiesToSpawn[i], templateToCloneEntityIdMap, *request.m_serializeContext); AZ_Assert(clone != nullptr, "Failed to clone spawnable entity."); spawnedEntities.emplace_back(clone); @@ -311,16 +298,8 @@ namespace AzFramework } // loadAll is true if every entity has been spawned only once - if (spawnedEntities.size() == entitiesToSpawnSize) - { - ticket.m_loadAll = true; - } - else - { - // Case where there were already spawns from a previous request - ticket.m_loadAll = false; - } - + ticket.m_loadAll = (spawnedEntities.size() == entitiesToSpawnSize); + // Let other systems know about newly spawned entities for any pre-processing before adding to the scene/game context. if (request.m_preInsertionCallback) { @@ -329,11 +308,10 @@ namespace AzFramework } // Add to the game context, now the entities are active - AZStd::for_each(ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end(), - [](AZ::Entity* entity) + for (auto it = ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount; it != ticket.m_spawnedEntities.end(); ++it) { - GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, entity); - }); + GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, *it); + } // Let other systems know about newly spawned entities for any post-processing after adding to the scene/game context. if (request.m_completionCallback) @@ -360,14 +338,34 @@ namespace AzFramework { AZStd::vector& spawnedEntities = ticket.m_spawnedEntities; AZStd::vector& spawnedEntityIndices = ticket.m_spawnedEntityIndices; + AZ_Assert( + spawnedEntities.size() == spawnedEntityIndices.size(), + "The indices for the spawned entities has gone out of sync with the entities."); - // Keep track how many entities there were in the array initially + // Keep track of how many entities there were in the array initially size_t spawnedEntitiesInitialCount = spawnedEntities.size(); // These are 'template' entities we'll be cloning from const Spawnable::EntityList& entitiesToSpawn = ticket.m_spawnable->GetEntities(); size_t entitiesToSpawnSize = request.m_entityIndices.size(); + // Reconstruct the template to entity mapping. + EntityIdMap templateToCloneEntityIdMap; + if (!request.m_referencePreviouslySpawnedEntities) + { + templateToCloneEntityIdMap.reserve(entitiesToSpawnSize); + } + else + { + templateToCloneEntityIdMap.reserve(spawnedEntitiesInitialCount + entitiesToSpawnSize); + SpawnableConstIndexEntityContainerView indexEntityView( + spawnedEntities.begin(), spawnedEntityIndices.begin(), spawnedEntities.size()); + for (auto& entry : indexEntityView) + { + templateToCloneEntityIdMap.insert_or_assign(entitiesToSpawn[entry.GetIndex()]->GetId(), entry.GetEntity()->GetId()); + } + } + spawnedEntities.reserve(spawnedEntities.size() + entitiesToSpawnSize); spawnedEntityIndices.reserve(spawnedEntityIndices.size() + entitiesToSpawnSize); @@ -375,15 +373,11 @@ namespace AzFramework { if (index < entitiesToSpawn.size()) { - const AZ::Entity& entityTemplate = *entitiesToSpawn[index]; - - AZ::Entity* clone = request.m_serializeContext->CloneObject(&entityTemplate); + AZ::Entity* clone = CloneSingleEntity(*entitiesToSpawn[index], templateToCloneEntityIdMap, *request.m_serializeContext); AZ_Assert(clone != nullptr, "Failed to clone spawnable entity."); - clone->SetId(AZ::Entity::MakeId()); spawnedEntities.push_back(clone); spawnedEntityIndices.push_back(index); - } } ticket.m_loadAll = false; @@ -396,11 +390,10 @@ namespace AzFramework } // Add to the game context, now the entities are active - AZStd::for_each(ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end(), - [](AZ::Entity* entity) + for (auto it = ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount; it != ticket.m_spawnedEntities.end(); ++it) { - GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, entity); - }); + GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, *it); + } if (request.m_completionCallback) { @@ -475,39 +468,43 @@ namespace AzFramework // Rebuild the list of entities. ticket.m_spawnedEntities.clear(); const Spawnable::EntityList& entities = request.m_spawnable->GetEntities(); + + // Map keeps track of ids from template (spawnable) to clone (instance) + // Allowing patch ups of fields referring to entityIds outside of a given entity + EntityIdMap templateToCloneEntityIdMap; + if (ticket.m_loadAll) { // The new spawnable may have a different number of entities and since the intent of the user was - // to load every, simply start over. + // to spawn every entity, simply start over. ticket.m_spawnedEntityIndices.clear(); - size_t entitiesToSpawnSize = entities.size(); - - // Map keeps track of ids from template (spawnable) to clone (instance) - // Allowing patch ups of fields referring to entityIds outside of a given entity - EntityIdMap templateToCloneEntityIdMap; templateToCloneEntityIdMap.reserve(entitiesToSpawnSize); - // Mark all indices as spawned for (size_t i = 0; i < entitiesToSpawnSize; ++i) { - const AZ::Entity& entityTemplate = *entities[i]; - - AZ::Entity* clone = CloneSingleEntity(entityTemplate, templateToCloneEntityIdMap, *request.m_serializeContext); - + AZ::Entity* clone = CloneSingleEntity(*entities[i], templateToCloneEntityIdMap, *request.m_serializeContext); AZ_Assert(clone != nullptr, "Failed to clone spawnable entity."); - ticket.m_spawnedEntities.emplace_back(clone); + ticket.m_spawnedEntities.push_back(clone); ticket.m_spawnedEntityIndices.push_back(i); } } else { size_t entitiesSize = entities.size(); + templateToCloneEntityIdMap.reserve(entitiesSize); for (size_t index : ticket.m_spawnedEntityIndices) { - ticket.m_spawnedEntities.push_back( - index < entitiesSize ? SpawnSingleEntity(*entities[index], *request.m_serializeContext) : nullptr); + // It's possible for the new spawnable to have a different number of entities, so guard against this. + // It's also possible that the entities have moved within the spawnable to a new index. This can't be + // detected and will result in the incorrect entities being spawned. + if (index < entitiesSize) + { + AZ::Entity* clone = CloneSingleEntity(*entities[index], templateToCloneEntityIdMap, *request.m_serializeContext); + AZ_Assert(clone != nullptr, "Failed to clone spawnable entity."); + ticket.m_spawnedEntities.push_back(clone); + } } } ticket.m_spawnable = AZStd::move(request.m_spawnable); diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h index e6db19557f..638559f3f1 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h @@ -37,7 +37,7 @@ namespace AzFramework AZ_CLASS_ALLOCATOR(SpawnableEntitiesManager, AZ::SystemAllocator, 0); using EntityIdMap = AZStd::unordered_map; - + enum class CommandQueueStatus : bool { HasCommandsLeft, @@ -57,7 +57,7 @@ namespace AzFramework // The following functions are thread safe // - void SpawnAllEntities(EntitySpawnTicket& ticket, SpawnEntitiesOptionalArgs optionalArgs = {}) override; + void SpawnAllEntities(EntitySpawnTicket& ticket, SpawnAllEntitiesOptionalArgs optionalArgs = {}) override; void SpawnEntities( EntitySpawnTicket& ticket, AZStd::vector entityIndices, SpawnEntitiesOptionalArgs optionalArgs = {}) override; void DespawnAllEntities(EntitySpawnTicket& ticket, DespawnAllEntitiesOptionalArgs optionalArgs = {}) override; @@ -114,6 +114,7 @@ namespace AzFramework Ticket* m_ticket; EntitySpawnTicket::Id m_ticketId; uint32_t m_requestId; + bool m_referencePreviouslySpawnedEntities; }; struct DespawnAllEntitiesCommand { @@ -183,12 +184,9 @@ namespace AzFramework CommandQueueStatus ProcessQueue(Queue& queue); - AZ::Entity* SpawnSingleEntity(const AZ::Entity& entityTemplate, - AZ::SerializeContext& serializeContext); - - AZ::Entity* CloneSingleEntity(const AZ::Entity& entityTemplate, - EntityIdMap& templateToCloneEntityIdMap, AZ::SerializeContext& serializeContext); - + AZ::Entity* CloneSingleEntity( + const AZ::Entity& entityTemplate, EntityIdMap& templateToCloneMap, AZ::SerializeContext& serializeContext); + bool ProcessRequest(SpawnAllEntitiesCommand& request); bool ProcessRequest(SpawnEntitiesCommand& request); bool ProcessRequest(DespawnAllEntitiesCommand& request); diff --git a/Code/Framework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp b/Code/Framework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp index 8637684b3c..32ad7d8ea9 100644 --- a/Code/Framework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp +++ b/Code/Framework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include namespace UnitTest @@ -81,6 +82,42 @@ namespace UnitTest } } + void CreateRecursiveHierarchy() + { + AzFramework::Spawnable::EntityList& entities = m_spawnable->GetEntities(); + size_t numElements = entities.size(); + AZ::EntityId parent; + for (size_t i=0; i& entity = entities[i]; + auto component = entity->CreateComponent(); + if (i > 0) + { + component->SetParent(parent); + } + parent = entity->GetId(); + } + } + + void CreateSingleParent() + { + AzFramework::Spawnable::EntityList& entities = m_spawnable->GetEntities(); + size_t numElements = entities.size(); + if (numElements > 0) + { + AZ::EntityId parent = entities[0]->GetId(); + for (size_t i = 0; i < numElements; ++i) + { + AZStd::unique_ptr& entity = entities[i]; + auto component = entity->CreateComponent(); + if (i > 0) + { + component->SetParent(parent); + } + } + } + } + protected: AZ::Data::Asset* m_spawnableAsset { nullptr }; AzFramework::SpawnableEntitiesManager* m_manager { nullptr }; @@ -104,7 +141,7 @@ namespace UnitTest { spawnedEntitiesCount += entities.size(); }; - AzFramework::SpawnEntitiesOptionalArgs optionalArgs; + AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs; optionalArgs.m_completionCallback = AZStd::move(callback); m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs)); m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); @@ -112,6 +149,37 @@ namespace UnitTest EXPECT_EQ(NumEntities, spawnedEntitiesCount); } + TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_SetParentOnSpawnedEntities_LineageIsPreserved) + { + static constexpr size_t NumEntities = 4; + FillSpawnable(NumEntities); + CreateRecursiveHierarchy(); + + auto callback = [](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) + { + AZ::EntityId parentId; + bool isFirst = true; + for (const AZ::Entity* entity : entities) + { + if (!isFirst) + { + auto transform = entity->GetTransform(); + ASSERT_NE(nullptr, transform); + EXPECT_EQ(parentId, transform->GetParentId()); + } + else + { + isFirst = false; + } + parentId = entity->GetId(); + } + }; + AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs; + optionalArgs.m_completionCallback = AZStd::move(callback); + m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs)); + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + } + TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_DeleteTicketBeforeCall_NoCrash) { { @@ -126,6 +194,170 @@ namespace UnitTest // SpawnEntities // + TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_Call_AllEntitiesSpawned) + { + static constexpr size_t NumEntities = 4; + FillSpawnable(NumEntities); + + AZStd::vector indices = { 0, 2, 3, 1 }; + + size_t spawnedEntitiesCount = 0; + auto callback = [&spawnedEntitiesCount](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) + { + spawnedEntitiesCount += entities.size(); + }; + AzFramework::SpawnEntitiesOptionalArgs optionalArgs; + optionalArgs.m_completionCallback = AZStd::move(callback); + m_manager->SpawnEntities(*m_ticket, AZStd::move(indices), AZStd::move(optionalArgs)); + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + + EXPECT_EQ(NumEntities, spawnedEntitiesCount); + } + + TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_SpawnTheSameEntity_AllEntitiesSpawned) + { + static constexpr size_t NumEntities = 1; + FillSpawnable(NumEntities); + + AZStd::vector indices = { 0, 0 }; + + size_t spawnedEntitiesCount = 0; + auto callback = + [&spawnedEntitiesCount](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) + { + spawnedEntitiesCount += entities.size(); + }; + AzFramework::SpawnEntitiesOptionalArgs optionalArgs; + optionalArgs.m_completionCallback = AZStd::move(callback); + m_manager->SpawnEntities(*m_ticket, AZStd::move(indices), AZStd::move(optionalArgs)); + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + + EXPECT_EQ(NumEntities * 2, spawnedEntitiesCount); + } + + TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_MultipleSpawns_AllEntitiesSpawned) + { + static constexpr size_t NumEntities = 4; + FillSpawnable(NumEntities); + + AZStd::vector indices = { 0, 2, 3, 1 }; + + size_t spawnedEntitiesCount = 0; + auto callback = + [&spawnedEntitiesCount](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) + { + spawnedEntitiesCount += entities.size(); + }; + AzFramework::SpawnEntitiesOptionalArgs optionalArgs; + optionalArgs.m_completionCallback = AZStd::move(callback); + m_manager->SpawnEntities(*m_ticket, indices, optionalArgs); + m_manager->SpawnEntities(*m_ticket, AZStd::move(indices), AZStd::move(optionalArgs)); + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + + EXPECT_EQ(NumEntities * 2, spawnedEntitiesCount); + } + + TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_ReferencesAreRemappedForNewBatch_AllPointToLatestParent) + { + static constexpr size_t NumEntities = 4; + FillSpawnable(NumEntities); + CreateSingleParent(); + + AZStd::vector indices = { 0, 1, 2, 3 }; + AZStd::vector parents; + + auto callback = [&parents](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) + { + AZ::EntityId parent = (*entities.begin())->GetId(); + parents.push_back(parent); + auto it = entities.begin(); + ++it; // Skip the first as that is the parent. + for (; it != entities.end(); ++it) + { + AZ::TransformInterface* transform = (*it)->GetTransform(); + ASSERT_NE(nullptr, transform); + ASSERT_EQ(parent, transform->GetParentId()); + } + }; + AzFramework::SpawnEntitiesOptionalArgs optionalArgs; + optionalArgs.m_completionCallback = AZStd::move(callback); + optionalArgs.m_referencePreviouslySpawnedEntities = false; + m_manager->SpawnEntities(*m_ticket, indices, optionalArgs); + m_manager->SpawnEntities(*m_ticket, AZStd::move(indices), AZStd::move(optionalArgs)); + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + + EXPECT_NE(parents[0], parents[1]); + } + + TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_ReferencesAreRemappedForContinuedBatch_AllPointToLatestParent) + { + static constexpr size_t NumEntities = 4; + FillSpawnable(NumEntities); + CreateSingleParent(); + + AZStd::vector indices = { 0, 1, 2, 3 }; + AZStd::vector parents; + + auto callback = + [&parents](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) + { + AZ::EntityId parent = (*entities.begin())->GetId(); + parents.push_back(parent); + auto it = entities.begin(); + ++it; // Skip the first as that is the parent. + for (; it!=entities.end(); ++it) + { + AZ::TransformInterface* transform = (*it)->GetTransform(); + ASSERT_NE(nullptr, transform); + ASSERT_EQ(parent, transform->GetParentId()); + } + }; + AzFramework::SpawnEntitiesOptionalArgs optionalArgs; + optionalArgs.m_completionCallback = AZStd::move(callback); + optionalArgs.m_referencePreviouslySpawnedEntities = true; + m_manager->SpawnEntities(*m_ticket, indices, optionalArgs); + m_manager->SpawnEntities(*m_ticket, AZStd::move(indices), AZStd::move(optionalArgs)); + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + + EXPECT_NE(parents[0], parents[1]); + } + + TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_ReferencesAreRemappedAcrossBatches_AllPointToLatestParent) + { + FillSpawnable(4); + CreateSingleParent(); + + // Spawn a regular batch but with two parents and store the id of the last entity. This will the parent for the next batch. + AZ::EntityId parent; + auto getParent = [&parent](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) + { + ASSERT_NE(entities.begin(), entities.end()); + parent = (*AZStd::prev(entities.end()))->GetId(); + }; + + AzFramework::SpawnEntitiesOptionalArgs optionalArgsFirstBatch; + optionalArgsFirstBatch.m_completionCallback = AZStd::move(getParent); + optionalArgsFirstBatch.m_referencePreviouslySpawnedEntities = true; + m_manager->SpawnEntities(*m_ticket, {0, 1, 2, 3, 0}, AZStd::move(optionalArgsFirstBatch)); + + // Next, spawn all the entities that have a reference to the parent that was just stored. + auto parentCheck = [&parent](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) + { + for (auto& it : entities) + { + AZ::TransformInterface* transform = it->GetTransform(); + ASSERT_NE(nullptr, transform); + ASSERT_EQ(parent, transform->GetParentId()); + } + }; + AzFramework::SpawnEntitiesOptionalArgs optionalArgsSecondBatch; + optionalArgsSecondBatch.m_completionCallback = AZStd::move(parentCheck); + optionalArgsSecondBatch.m_referencePreviouslySpawnedEntities = true; + m_manager->SpawnEntities(*m_ticket, {1, 2, 3}, AZStd::move(optionalArgsSecondBatch)); + + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + } + TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_DeleteTicketBeforeCall_NoCrash) { { @@ -186,7 +418,7 @@ namespace UnitTest }; m_manager->SpawnAllEntities(*m_ticket); - m_manager->ListEntities(*m_ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback)); + m_manager->ListEntities(*m_ticket, AZStd::move(callback)); m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); EXPECT_TRUE(allValidEntityIds); @@ -199,7 +431,7 @@ namespace UnitTest { AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset); - m_manager->ListEntities(ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback)); + m_manager->ListEntities(ticket, AZStd::move(callback)); } m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); } @@ -231,7 +463,7 @@ namespace UnitTest }; m_manager->SpawnAllEntities(*m_ticket); - m_manager->ListIndicesAndEntities(*m_ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback)); + m_manager->ListIndicesAndEntities(*m_ticket, AZStd::move(callback)); m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); EXPECT_TRUE(allValidEntityIds); @@ -244,7 +476,7 @@ namespace UnitTest { AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset); - m_manager->ListIndicesAndEntities(ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback)); + m_manager->ListIndicesAndEntities(ticket, AZStd::move(callback)); } m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); } @@ -260,7 +492,7 @@ namespace UnitTest { AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset); - m_manager->ClaimEntities(ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback)); + m_manager->ClaimEntities(ticket, AZStd::move(callback)); } m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); } @@ -276,7 +508,7 @@ namespace UnitTest { AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset); - m_manager->Barrier(ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback)); + m_manager->Barrier(ticket, AZStd::move(callback)); } m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); } @@ -307,12 +539,12 @@ namespace UnitTest defaultPriorityCallId = callCounter++; }; - AzFramework::SpawnEntitiesOptionalArgs optionalArgs; + AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs; optionalArgs.m_completionCallback = AZStd::move(defaultCallback); optionalArgs.m_priority = AzFramework::SpawnablePriority_Default; m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs)); - AzFramework::SpawnEntitiesOptionalArgs highPriortyOptionalArgs; + AzFramework::SpawnAllEntitiesOptionalArgs highPriortyOptionalArgs; highPriortyOptionalArgs.m_completionCallback = AZStd::move(highCallback); highPriortyOptionalArgs.m_priority = AzFramework::SpawnablePriority_High; m_manager->SpawnAllEntities(highPriorityTicket, AZStd::move(highPriortyOptionalArgs)); @@ -343,12 +575,12 @@ namespace UnitTest defaultPriorityCallId = callCounter++; }; - AzFramework::SpawnEntitiesOptionalArgs optionalArgs; + AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs; optionalArgs.m_completionCallback = AZStd::move(defaultCallback); optionalArgs.m_priority = AzFramework::SpawnablePriority_Default; m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs)); - AzFramework::SpawnEntitiesOptionalArgs highPriortyOptionalArgs; + AzFramework::SpawnAllEntitiesOptionalArgs highPriortyOptionalArgs; highPriortyOptionalArgs.m_completionCallback = AZStd::move(highCallback); highPriortyOptionalArgs.m_priority = AzFramework::SpawnablePriority_High; m_manager->SpawnAllEntities(*m_ticket, AZStd::move(highPriortyOptionalArgs)); diff --git a/Gems/Multiplayer/Code/Source/Pipeline/NetBindMarkerComponent.cpp b/Gems/Multiplayer/Code/Source/Pipeline/NetBindMarkerComponent.cpp index c93c09cfbc..bd1cf40da8 100644 --- a/Gems/Multiplayer/Code/Source/Pipeline/NetBindMarkerComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Pipeline/NetBindMarkerComponent.cpp @@ -81,8 +81,10 @@ namespace Multiplayer }; m_netSpawnTicket = AzFramework::EntitySpawnTicket(m_networkSpawnableAsset); + AzFramework::SpawnEntitiesOptionalArgs optionalArgs; + optionalArgs.m_preInsertionCallback = AZStd::move(preInsertionCallback); AzFramework::SpawnableEntitiesInterface::Get()->SpawnEntities( - m_netSpawnTicket, AzFramework::SpawnablePriority_Default, { m_netEntityIndex }, preInsertionCallback); + m_netSpawnTicket, { m_netEntityIndex }, AZStd::move(optionalArgs)); } } @@ -90,7 +92,7 @@ namespace Multiplayer { if(m_netSpawnTicket.IsValid()) { - AzFramework::SpawnableEntitiesInterface::Get()->DespawnAllEntities(m_netSpawnTicket, AzFramework::SpawnablePriority_Default); + AzFramework::SpawnableEntitiesInterface::Get()->DespawnAllEntities(m_netSpawnTicket); } } diff --git a/Gems/ScriptCanvas/Code/Include/ScriptCanvas/Libraries/Spawning/SpawnNodeable.cpp b/Gems/ScriptCanvas/Code/Include/ScriptCanvas/Libraries/Spawning/SpawnNodeable.cpp index 39afa46a19..a7875c2615 100644 --- a/Gems/ScriptCanvas/Code/Include/ScriptCanvas/Libraries/Spawning/SpawnNodeable.cpp +++ b/Gems/ScriptCanvas/Code/Include/ScriptCanvas/Libraries/Spawning/SpawnNodeable.cpp @@ -134,7 +134,7 @@ namespace ScriptCanvas::Nodeables::Spawning m_spawnBatchSizes.push_back(view.size()); }; - AzFramework::SpawnEntitiesOptionalArgs optionalArgs; + AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs; optionalArgs.m_preInsertionCallback = AZStd::move(preSpawnCB); optionalArgs.m_completionCallback = AZStd::move(spawnCompleteCB); AzFramework::SpawnableEntitiesInterface::Get()->SpawnAllEntities(m_spawnTicket, AZStd::move(optionalArgs));