From 48f2487d3c45416f5ed3499abcf409090fac36dc Mon Sep 17 00:00:00 2001 From: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> Date: Mon, 25 Oct 2021 13:43:00 -0700 Subject: [PATCH 01/14] Updates in preparation for adding entity aliases to spawnables. The following has been changed: - AssetDataStream can now return the stored streaming deadline and priority. - RootSpawnable now has an event that's called just before root spawnable spawns entities. This is an immediate event unlike the other events that are queued. Signed-off-by: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> --- .../AzCore/AzCore/Asset/AssetDataStream.h | 3 +++ .../Spawnable/RootSpawnableInterface.h | 10 ++++++++ .../Spawnable/SpawnableSystemComponent.cpp | 25 +++++++++++++++---- .../Spawnable/SpawnableSystemComponent.h | 1 + .../PrefabEditorEntityOwnershipService.cpp | 22 ++++++++-------- 5 files changed, 45 insertions(+), 16 deletions(-) diff --git a/Code/Framework/AzCore/AzCore/Asset/AssetDataStream.h b/Code/Framework/AzCore/AzCore/Asset/AssetDataStream.h index 62f5808207..d2b069b55b 100644 --- a/Code/Framework/AzCore/AzCore/Asset/AssetDataStream.h +++ b/Code/Framework/AzCore/AzCore/Asset/AssetDataStream.h @@ -70,6 +70,9 @@ namespace AZ::Data const char* GetFilename() const override { return m_filePath.c_str(); } + AZStd::chrono::milliseconds GetStreamingDeadline() const { return m_curDeadline; } + AZ::IO::IStreamerTypes::Priority GetStreamingPriority() const { return m_curPriority; } + // AssetDataStream specific APIs //! Whether or not all data has been loaded. diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/RootSpawnableInterface.h b/Code/Framework/AzFramework/AzFramework/Spawnable/RootSpawnableInterface.h index 72a3031e3e..873123f38b 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/RootSpawnableInterface.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/RootSpawnableInterface.h @@ -30,12 +30,22 @@ namespace AzFramework //! Called when the root spawnable has been assigned a new value. This may be called several times without a call to release //! in between. + //! NOTE: The callback is not queued but immediately called from a random thread. This is done because this callback is typically + //! used before entities are spawned and if it's queued then the entities spawn before this callback is called. //! @param rootSpawnable The new root spawnable that was assigned. //! @param generation The generation of the root spawnable. This will increment every time a new spawnable is assigned. virtual void OnRootSpawnableAssigned([[maybe_unused]] AZ::Data::Asset rootSpawnable, [[maybe_unused]] uint32_t generation) {} + //! Called when the root spawnable has completed spawning of entities. This may be called several times without a call to release + //! in between. + //! NOTE: This callback is queued and will be called with a delay and from the main thread. + //! @param rootSpawnable The new root spawnable that was used to spawn entities from. + //! @param generation The generation of the root spawnable. This will increment every time a new spawnable is assigned. + virtual void OnRootSpawnableReady( + [[maybe_unused]] AZ::Data::Asset rootSpawnable, [[maybe_unused]] uint32_t generation) {} //! Called when the root spawnable has Released. This will only be called if there's no root spawnable assigned to take the //! place of the original root spawnable. + //! Note: This callback is queued and will be called with a delay and from the main thread. //! @param generation The generation of the root spawnable that was released. virtual void OnRootSpawnableReleased([[maybe_unused]] uint32_t generation) {} }; diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableSystemComponent.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableSystemComponent.cpp index 957786c6df..6ca2f3a53a 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableSystemComponent.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableSystemComponent.cpp @@ -72,7 +72,7 @@ namespace AzFramework uint64_t SpawnableSystemComponent::AssignRootSpawnable(AZ::Data::Asset rootSpawnable) { - uint64_t generation = 0; + uint32_t generation = 0; if (m_rootSpawnableId == rootSpawnable.GetId()) { @@ -87,16 +87,25 @@ namespace AzFramework // Suspend and resume processing in the container that completion calls aren't received until // everything has been setup to accept callbacks from the call. m_rootSpawnableContainer.Reset(rootSpawnable); - m_rootSpawnableContainer.SpawnAllEntities(); generation = m_rootSpawnableContainer.GetCurrentGeneration(); - AZ_TracePrintf("Spawnables", "Root spawnable set to '%s' at generation %zu.\n", rootSpawnable.GetHint().c_str(), - generation); + + // Don't send out the alert that the root spawnable has been assigned until the spawnable itself is ready. The common + // use case is for handlers to do something with the information in the spawnable before the entities get spawned. + m_rootSpawnableContainer.Alert( + [rootSpawnable](uint32_t generation) + { + RootSpawnableNotificationBus::Broadcast( + &RootSpawnableNotificationBus::Events::OnRootSpawnableAssigned, AZStd::move(rootSpawnable), generation); + }, SpawnableEntitiesContainer::CheckIfSpawnableIsLoaded::Yes); + m_rootSpawnableContainer.SpawnAllEntities(); m_rootSpawnableContainer.Alert( [newSpawnable = AZStd::move(rootSpawnable)](uint32_t generation) { RootSpawnableNotificationBus::QueueBroadcast( - &RootSpawnableNotificationBus::Events::OnRootSpawnableAssigned, newSpawnable, generation); + &RootSpawnableNotificationBus::Events::OnRootSpawnableReady, AZStd::move(newSpawnable), generation); }); + + AZ_TracePrintf("Spawnables", "Root spawnable set to '%s' at generation %zu.\n", rootSpawnable.GetHint().c_str(), generation); } else { @@ -132,6 +141,12 @@ namespace AzFramework AZ_TracePrintf("Spawnables", "New root spawnable '%s' assigned (generation: %i).\n", rootSpawnable.GetHint().c_str(), generation); } + void SpawnableSystemComponent::OnRootSpawnableReady( + [[maybe_unused]] AZ::Data::Asset rootSpawnable, [[maybe_unused]] uint32_t generation) + { + AZ_TracePrintf("Spawnables", "Entities from new root spawnable '%s' are ready (generation: %i).\n", rootSpawnable.GetHint().c_str(), generation); + } + void SpawnableSystemComponent::OnRootSpawnableReleased([[maybe_unused]] uint32_t generation) { AZ_TracePrintf("Spawnables", "Generation %i of the root spawnable has been released.\n", generation); diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableSystemComponent.h b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableSystemComponent.h index 74e255d624..ecd2a9b728 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableSystemComponent.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableSystemComponent.h @@ -82,6 +82,7 @@ namespace AzFramework // void OnRootSpawnableAssigned(AZ::Data::Asset rootSpawnable, uint32_t generation) override; + void OnRootSpawnableReady(AZ::Data::Asset rootSpawnable, uint32_t generation) override; void OnRootSpawnableReleased(uint32_t generation) override; protected: diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.cpp index 67b5d99011..97953bac18 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.cpp @@ -581,15 +581,15 @@ namespace AzToolsFramework { if (m_rootInstance && m_playInEditorData.m_isEnabled) { - AZ_Assert(m_playInEditorData.m_entities.IsSet(), + AZ_Assert( + m_playInEditorData.m_entities.IsSet(), "Invalid Game Mode Entities Container encountered after play-in-editor stopped. " "Confirm that the container was initialized correctly"); m_playInEditorData.m_entities.DespawnAllEntities(); m_playInEditorData.m_entities.Alert( [assets = AZStd::move(m_playInEditorData.m_assets), - deactivatedEntities = AZStd::move(m_playInEditorData.m_deactivatedEntities)] - ([[maybe_unused]]uint32_t generation) mutable + deactivatedEntities = AZStd::move(m_playInEditorData.m_deactivatedEntities)]([[maybe_unused]] uint32_t generation) mutable { auto end = deactivatedEntities.rend(); for (auto it = deactivatedEntities.rbegin(); it != end; ++it) @@ -614,15 +614,15 @@ namespace AzToolsFramework AzFramework::GameEntityContextEventBus::Broadcast(&AzFramework::GameEntityContextEventBus::Events::OnGameEntitiesReset); }); m_playInEditorData.m_entities.Clear(); - } - // Game entity cleanup is queued onto the next tick via the DespawnEntities call. - // To avoid both game entities and Editor entities active at the same time - // we flush the tick queue to ensure the game entities are cleared first. - // The Alert callback that follows the DespawnEntities call will then reactivate the editor entities - // This should be considered temporary as a move to a less rigid event sequence that supports async entity clean up - // is the desired direction forward. - AZ::TickBus::ExecuteQueuedEvents(); + // Game entity cleanup is queued onto the next tick via the DespawnEntities call. + // To avoid both game entities and Editor entities active at the same time + // we flush the tick queue to ensure the game entities are cleared first. + // The Alert callback that follows the DespawnEntities call will then reactivate the editor entities + // This should be considered temporary as a move to a less rigid event sequence that supports async entity clean up + // is the desired direction forward. + AZ::TickBus::ExecuteQueuedEvents(); + } m_playInEditorData.m_isEnabled = false; } From a0d7048fd4dced2fa0b216aff2a9cae8f1bf9cc5 Mon Sep 17 00:00:00 2001 From: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> Date: Mon, 25 Oct 2021 14:07:14 -0700 Subject: [PATCH 02/14] Added support for entity aliases to Spawnable. Entity aliases can be used to have a request to spawn an entity: - spawn the original entity as normal - be disabled - redirected to another entity in another spawnable - also spawn an entity from another spawnable - add the components from an entity in another spawnable An entity alias can indicate whether or not to load the spawnable dependency. If the spawnable dependency is loaded it will be loaded asynchronously because starting blocking loads in an asset handler can lead to deadlocks once there are no more jobs available to deserialize assets. Signed-off-by: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> --- .../AzFramework/Spawnable/Spawnable.cpp | 494 +++++++++++++++++- .../AzFramework/Spawnable/Spawnable.h | 152 +++++- .../AzFramework/Spawnable/SpawnableAssetBus.h | 38 ++ .../Spawnable/SpawnableAssetHandler.cpp | 39 ++ .../Spawnable/SpawnableAssetHandler.h | 8 + .../AzFramework/azframework_files.cmake | 1 + 6 files changed, 727 insertions(+), 5 deletions(-) create mode 100644 Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableAssetBus.h diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp index 4855dc15b3..2be76c28a6 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp @@ -8,10 +8,447 @@ #include #include +#include #include namespace AzFramework { + // + // EntityAlias + // + + + bool Spawnable::EntityAlias::HasLowerIndex(const EntityAlias& other) const + { + return m_sourceIndex == other.m_sourceIndex ? + m_aliasType < other.m_aliasType : + m_sourceIndex < other.m_sourceIndex; + } + + + // + // EntityAliasVisitorBase + // + + bool Spawnable::EntityAliasVisitorBase::HasLock(const EntityAliasList* aliases) const + { + return aliases != nullptr; + } + + bool Spawnable::EntityAliasVisitorBase::HasAliases(const EntityAliasList* aliases) const + { + AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked."); + return !aliases->empty(); + } + + bool Spawnable::EntityAliasVisitorBase::AreAllSpawnablesReady(const EntityAliasList* aliases) const + { + AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked."); + for (const EntityAlias& alias : *aliases) + { + if ((alias.m_aliasType != Spawnable::EntityAliasType::Original && alias.m_aliasType != Spawnable::EntityAliasType::Disabled) && + !alias.m_spawnable.IsReady()) + { + return false; + } + } + return true; + } + + auto Spawnable::EntityAliasVisitorBase::begin(const EntityAliasList* aliases) const -> EntityAliasList::const_iterator + { + AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked."); + return aliases->cbegin(); + } + + auto Spawnable::EntityAliasVisitorBase::end(const EntityAliasList* aliases) const -> EntityAliasList::const_iterator + { + AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked."); + return aliases->cend(); + } + + auto Spawnable::EntityAliasVisitorBase::cbegin(const EntityAliasList* aliases) const -> EntityAliasList::const_iterator + { + AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked."); + return aliases->cbegin(); + } + + auto Spawnable::EntityAliasVisitorBase::cend(const EntityAliasList* aliases) const -> EntityAliasList::const_iterator + { + AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked."); + return aliases->cend(); + } + + void Spawnable::EntityAliasVisitorBase::ListTargetSpawnables( + const EntityAliasList* aliases, const ListTargetSpawanblesCallback& callback) const + { + AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked."); + AZStd::unordered_set spawnableIds; + for (const Spawnable::EntityAlias& alias : *aliases) + { + auto it = spawnableIds.find(alias.m_spawnable.GetId()); + if (it == spawnableIds.end()) + { + callback(alias.m_spawnable); + spawnableIds.emplace(alias.m_spawnable.GetId()); + } + } + } + + void Spawnable::EntityAliasVisitorBase::ListTargetSpawnables( + const EntityAliasList* aliases, AZ::Crc32 tag, const ListTargetSpawanblesCallback& callback) const + { + AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked."); + AZStd::unordered_set spawnableIds; + for (const Spawnable::EntityAlias& alias : *aliases) + { + if (alias.m_tag == tag) + { + auto it = spawnableIds.find(alias.m_spawnable.GetId()); + if (it == spawnableIds.end()) + { + callback(alias.m_spawnable); + spawnableIds.emplace(alias.m_spawnable.GetId()); + } + } + } + } + + + // + // EntityAliasVisitor + // + + + Spawnable::EntityAliasVisitor::EntityAliasVisitor(Spawnable& owner, EntityAliasList* entityAliasList) + : m_owner(owner) + , m_entityAliasList(entityAliasList) + { + } + + Spawnable::EntityAliasVisitor::~EntityAliasVisitor() + { + if (HasLock()) + { + Optimize(); + + AZ_Assert( + m_owner.m_lockState == LockState::Locked, "Attempting to unlock a spawnable that's not in the locked state (%i).", + m_owner.m_lockState.load()); + m_owner.m_lockState = LockState::Unlocked; + } + } + + Spawnable::EntityAliasVisitor::EntityAliasVisitor(EntityAliasVisitor&& rhs) + : m_owner(rhs.m_owner) + , m_entityAliasList(rhs.m_entityAliasList) + { + m_dirty = rhs.m_dirty; + + rhs.m_entityAliasList = nullptr; + rhs.m_dirty = false; + } + + auto Spawnable::EntityAliasVisitor::operator=(EntityAliasVisitor&& rhs) -> EntityAliasVisitor& + { + if (this != &rhs) + { + this->~EntityAliasVisitor(); + *this = EntityAliasVisitor(rhs.m_owner, rhs.m_entityAliasList); + m_dirty = rhs.m_dirty; + + rhs.m_entityAliasList = nullptr; + rhs.m_dirty = false; + } + return *this; + } + + bool Spawnable::EntityAliasVisitor::HasLock() const + { + return EntityAliasVisitorBase::HasLock(m_entityAliasList); + } + + bool Spawnable::EntityAliasVisitor::HasAliases() const + { + return EntityAliasVisitorBase::HasAliases(m_entityAliasList); + } + + bool Spawnable::EntityAliasVisitor::AreAllSpawnablesReady() const + { + return EntityAliasVisitorBase::AreAllSpawnablesReady(m_entityAliasList); + } + + auto Spawnable::EntityAliasVisitor::begin() const -> EntityAliasList::const_iterator + { + return EntityAliasVisitorBase::begin(m_entityAliasList); + } + + auto Spawnable::EntityAliasVisitor::end() const -> EntityAliasList::const_iterator + { + return EntityAliasVisitorBase::end(m_entityAliasList); + } + + auto Spawnable::EntityAliasVisitor::cbegin() const -> EntityAliasList::const_iterator + { + return EntityAliasVisitorBase::cbegin(m_entityAliasList); + } + + auto Spawnable::EntityAliasVisitor::cend() const -> EntityAliasList::const_iterator + { + return EntityAliasVisitorBase::cend(m_entityAliasList); + } + + void Spawnable::EntityAliasVisitor::ListTargetSpawnables(const ListTargetSpawanblesCallback& callback) const + { + EntityAliasVisitorBase::ListTargetSpawnables(m_entityAliasList, callback); + } + + void Spawnable::EntityAliasVisitor::ListTargetSpawnables(AZ::Crc32 tag, const ListTargetSpawanblesCallback& callback) const + { + EntityAliasVisitorBase::ListTargetSpawnables(m_entityAliasList, tag, callback); + } + + void Spawnable::EntityAliasVisitor::AddAlias( + AZ::Data::Asset targetSpawnable, + AZ::Crc32 tag, + uint32_t sourceIndex, + uint32_t targetIndex, + Spawnable::EntityAliasType aliasType, + bool queueLoad) + { + AZ_Assert(m_entityAliasList, "Attempting to visit entity aliases on a spawnable that wasn't locked."); + AZ_Assert(sourceIndex < m_owner.GetEntities().size(), "Invalid source index (%i) for entity alias", sourceIndex); + if (targetSpawnable.IsReady()) + { + AZ_Assert( + targetIndex < targetSpawnable->GetEntities().size(), "Invalid target index (%i) for entity alias '%s'", targetIndex, + targetSpawnable.GetHint().c_str()); + } + + m_entityAliasList->push_back(Spawnable::EntityAlias{ targetSpawnable, tag, sourceIndex, targetIndex, aliasType, queueLoad }); + m_dirty = true; + } + + void Spawnable::EntityAliasVisitor::ListSpawnablesPendingLoad(const ListSpawnablesPendingLoadCallback& callback) + { + AZ_Assert(m_entityAliasList, "Attempting to visit entity aliases on a spawnable that wasn't locked."); + for (Spawnable::EntityAlias& alias : *m_entityAliasList) + { + if (alias.m_queueLoad && + alias.m_aliasType != Spawnable::EntityAliasType::Original && + alias.m_aliasType != Spawnable::EntityAliasType::Disabled && + !alias.m_spawnable.IsLoading() && + !alias.m_spawnable.IsReady() && + !alias.m_spawnable.IsError()) + { + callback(alias.m_spawnable); + } + } + } + + void Spawnable::EntityAliasVisitor::UpdateAliases(const UpdateCallback& callback) + { + AZ_Assert(m_entityAliasList, "Attempting to visit entity aliases on a spawnable that wasn't locked."); + for (Spawnable::EntityAlias& alias : *m_entityAliasList) + { + AZ::Data::Asset targetSpawnable(alias.m_spawnable); + callback(alias.m_aliasType, alias.m_queueLoad, targetSpawnable, alias.m_tag, alias.m_sourceIndex, alias.m_targetIndex); + } + m_dirty = true; + } + + void Spawnable::EntityAliasVisitor::UpdateAliases(AZ::Crc32 tag, const UpdateCallback& callback) + { + AZ_Assert(m_entityAliasList, "Attempting to visit entity aliases on a spawnable that wasn't locked."); + for (Spawnable::EntityAlias& alias : *m_entityAliasList) + { + if (alias.m_tag == tag) + { + AZ::Data::Asset targetSpawnable(alias.m_spawnable); + callback(alias.m_aliasType, alias.m_queueLoad, targetSpawnable, alias.m_tag, alias.m_sourceIndex, alias.m_targetIndex); + m_dirty = true; + } + } + } + + void Spawnable::EntityAliasVisitor::UpdateAliasType(uint32_t index, Spawnable::EntityAliasType newType) + { + AZ_Assert(m_entityAliasList, "Attempting to visit entity aliases on a spawnable that wasn't locked."); + AZ_Assert( + index < m_entityAliasList->size(), "Unable to update entity alias at index %i as there are only %zu aliases in spawnable.", + index, m_entityAliasList->size()); + (*m_entityAliasList)[index].m_aliasType = newType; + m_dirty = true; + } + + void Spawnable::EntityAliasVisitor::Optimize() + { + AZ_Assert(m_entityAliasList, "Attempting to visit entity aliases on a spawnable that wasn't locked."); + if (m_dirty) + { + AZStd::stable_sort( + m_entityAliasList->begin(), m_entityAliasList->end(), + [](const Spawnable::EntityAlias& lhs, const Spawnable::EntityAlias& rhs) + { + // Sort by source index from smallest to largest so during spawning the entities can be iterated linearly over. + // If the source index is the same then sort by alias type so the next steps can optimize away superfluous steps. + return lhs.HasLowerIndex(rhs); + }); + + // Remove aliases that are not going to have any practical effect and insert aliases where needed to simplify the spawning. + // This is done at runtime rather than at build time because the above ebus allows other systems to make adjustments to the + // aliases, for instance Networking can decide to disable certain aliases when running on a client. This in turn also requires + // the aliases to be in their recorded order during building as the ebus handlers may depend on that order to determine what + // entities need to be updated. + Spawnable::EntityAlias* compare = m_entityAliasList->begin(); + Spawnable::EntityAlias* it = m_entityAliasList->begin() + 1; + Spawnable::EntityAlias* end = m_entityAliasList->end(); + while (it < end) + { + switch (it->m_aliasType) + { + case Spawnable::EntityAliasType::Original: + // If this is the only alias for the entity then the original can be removed. + { + Spawnable::EntityAlias* next = it + 1; + if (next == end || next->m_sourceIndex != it->m_sourceIndex) + { + // Erase instead of a swap-and-pop in order to preserver the order. + m_entityAliasList->erase(compare); + --end; + break; + } + } + [[fallthrough]]; + case Spawnable::EntityAliasType::Disabled: + [[fallthrough]]; + case Spawnable::EntityAliasType::Replace: + // If the previous entry was a disabled, original or replace alias then remove it as it will be overwritten by the + // current entry. + if (compare->m_sourceIndex == it->m_sourceIndex && + (compare->m_aliasType == Spawnable::EntityAliasType::Original || + compare->m_aliasType == Spawnable::EntityAliasType::Disabled || + compare->m_aliasType == Spawnable::EntityAliasType::Replace)) + { + // Erase instead of a swap-and-pop in order to preserver the order. + m_entityAliasList->erase(compare); + --end; + } + else + { + ++compare; + ++it; + } + break; + case Spawnable::EntityAliasType::Additional: + [[fallthrough]]; + case Spawnable::EntityAliasType::Merge: + // If this is the first entry for this type insert an original in front of it so the spawnable entity manager + // does have to check for the case there's a merge and/or addition without a prefix. + if (compare->m_sourceIndex != it->m_sourceIndex) + { + Spawnable::EntityAlias insert; + // No load, as the asset is already loaded. + insert.m_spawnable = AZ::Data::Asset(&m_owner, AZ::Data::AssetLoadBehavior::NoLoad); + insert.m_sourceIndex = it->m_sourceIndex; + insert.m_targetIndex = it->m_sourceIndex; // Source index as the original entry for this slot is added. + insert.m_aliasType = Spawnable::EntityAliasType::Original; + m_entityAliasList->insert(compare, AZStd::move(insert)); + compare += 2; + it += 2; + ++end; + } + else + { + ++compare; + ++it; + } + break; + default: + AZ_Assert(false, "Invalid Spawnable entity alias type found during asset loading: %i", compare->m_aliasType); + break; + } + } + // Reclaim memory because after this point the aliases will not change anymore. + m_entityAliasList->shrink_to_fit(); + m_dirty = false; + } + } + + + + // + // EntityAliasConstVisitor + // + + Spawnable::EntityAliasConstVisitor::EntityAliasConstVisitor(const Spawnable& owner, const EntityAliasList* entityAliasList) + : m_owner(owner) + , m_entityAliasList(entityAliasList) + { + } + + Spawnable::EntityAliasConstVisitor::~EntityAliasConstVisitor() + { + if (HasLock()) + { + AZ_Assert( + m_owner.m_lockState < 0, "Attempting to unlock a read shared spawnable that was not in a read shared mode (%i).", + m_owner.m_lockState.load()); + m_owner.m_lockState++; + } + } + + bool Spawnable::EntityAliasConstVisitor::HasLock() const + { + return EntityAliasVisitorBase::HasLock(m_entityAliasList); + } + + bool Spawnable::EntityAliasConstVisitor::HasAliases() const + { + return EntityAliasVisitorBase::HasAliases(m_entityAliasList); + } + + bool Spawnable::EntityAliasConstVisitor::AreAllSpawnablesReady() const + { + return EntityAliasVisitorBase::AreAllSpawnablesReady(m_entityAliasList); + } + + auto Spawnable::EntityAliasConstVisitor::begin() const -> EntityAliasList::const_iterator + { + return EntityAliasVisitorBase::begin(m_entityAliasList); + } + + auto Spawnable::EntityAliasConstVisitor::end() const -> EntityAliasList::const_iterator + { + return EntityAliasVisitorBase::end(m_entityAliasList); + } + + auto Spawnable::EntityAliasConstVisitor::cbegin() const -> EntityAliasList::const_iterator + { + return EntityAliasVisitorBase::cbegin(m_entityAliasList); + } + + auto Spawnable::EntityAliasConstVisitor::cend() const -> EntityAliasList::const_iterator + { + return EntityAliasVisitorBase::cend(m_entityAliasList); + } + + void Spawnable::EntityAliasConstVisitor::ListTargetSpawnables(const ListTargetSpawanblesCallback& callback) const + { + EntityAliasVisitorBase::ListTargetSpawnables(m_entityAliasList, callback); + } + + void Spawnable::EntityAliasConstVisitor::ListTargetSpawnables(AZ::Crc32 tag, const ListTargetSpawanblesCallback& callback) const + { + EntityAliasVisitorBase::ListTargetSpawnables(m_entityAliasList, tag, callback); + } + + + + // + // Spawnable + // + Spawnable::Spawnable(const AZ::Data::AssetId& id, AssetStatus status) : AZ::Data::AssetData(id, status) { @@ -27,11 +464,56 @@ namespace AzFramework return m_entities; } + auto Spawnable::TryGetAliasesConst() const -> EntityAliasConstVisitor + { + int32_t expected = LockState::Unlocked; + do + { + // Try to set the lock to a negative number to indicate a shared read. + if (m_lockState.compare_exchange_strong(expected, expected - 1)) + { + return EntityAliasConstVisitor(*this, &m_entityAliases); + } + // as long as the value is negative keep trying to get a shared read lock. + } while (expected <= 0); + return EntityAliasConstVisitor(*this, nullptr); + } + + auto Spawnable::TryGetAliases() const -> EntityAliasConstVisitor + { + return TryGetAliasesConst(); + } + + auto Spawnable::TryGetAliases() -> EntityAliasVisitor + { + int32_t expected = LockState::Unlocked; + return m_lockState.compare_exchange_strong(expected, LockState::Locked) ? EntityAliasVisitor(*this, &m_entityAliases) + : EntityAliasVisitor(*this, nullptr); + } + bool Spawnable::IsEmpty() const { return m_entities.empty(); } + bool Spawnable::IsPermanentlyLocked() const + { + return m_lockState == LockState::PermanentLock; + } + + bool Spawnable::LockPermanently() + { + if (!IsPermanentlyLocked()) + { + int32_t expected = LockState::Unlocked; + return m_lockState.compare_exchange_strong(expected, LockState::PermanentLock); + } + else + { + return true; + } + } + SpawnableMetaData& Spawnable::GetMetaData() { return m_metaData; @@ -46,8 +528,18 @@ namespace AzFramework { if (auto* serializeContext = azrtti_cast(context); serializeContext != nullptr) { - serializeContext->Class()->Version(1) + serializeContext->Class() + ->Version(1) + ->Field("Spawnable", &Spawnable::EntityAlias::m_spawnable) + ->Field("Tag", &Spawnable::EntityAlias::m_tag) + ->Field("Source Index", &Spawnable::EntityAlias::m_sourceIndex) + ->Field("Target Index", &Spawnable::EntityAlias::m_targetIndex) + ->Field("Alias Type", &Spawnable::EntityAlias::m_aliasType) + ->Field("Queue Load", &Spawnable::EntityAlias::m_queueLoad); + + serializeContext->Class()->Version(2) ->Field("Meta data", &Spawnable::m_metaData) + ->Field("Entity aliases", &Spawnable::m_entityAliases) ->Field("Entities", &Spawnable::m_entities); } } diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h index 37c22d503d..05ae10e580 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -29,7 +30,139 @@ namespace AzFramework AZ_CLASS_ALLOCATOR(Spawnable, AZ::SystemAllocator, 0); AZ_RTTI(AzFramework::Spawnable, "{855E3021-D305-4845-B284-20C3F7FDF16B}", AZ::Data::AssetData); + // The order is important for sorting in the SpawnableAssetHandler. + enum class EntityAliasType : uint8_t + { + Original, //!< The original entity is spawned. + Disabled, //!< No entity will be spawned. + Replace, //!< The entity alias is spawned instead of the original. + Additional, //!< The original entity is spawned as well as the alias. The alias will get a new entity id. + Merge //!< The original entity is spawned and the components of the alias are added. The caller is responsible for + //!< maintaining a valid component list. + }; + + enum LockState : int32_t + { + Unlocked, + Locked, + PermanentLock + }; + + //! An entity alias redirects the spawning of an entity to another entity, possibly in another spawnable. + struct EntityAlias + { + AZ_CLASS_ALLOCATOR(EntityAlias, AZ::SystemAllocator, 0); + AZ_TYPE_INFO(AzFramework::Spawnable::EntityAlias, "{C8D0C5BC-1F0B-4572-98C1-73B2CA8C9356}"); + + bool HasLowerIndex(const EntityAlias& other) const; + + AZ::Data::Asset m_spawnable; //!< The spawnable containing the target entity to spawn. + uint32_t m_tag{ 0 }; //!< A unique tag to identify this alias with. + uint32_t m_sourceIndex{ 0 }; //!< The index of the entity in the original spawnable that will be replaced. + uint32_t m_targetIndex{ 0 }; //!< The index of the entity in the target spawnable that will be used to replace the original. + EntityAliasType m_aliasType{ EntityAliasType::Original }; //!< The kind of replacement. + bool m_queueLoad{ false }; //!< Whether or not to automatically queue the spawnable for loading. + }; + using EntityList = AZStd::vector>; + using EntityAliasList = AZStd::vector; + + private: + class EntityAliasVisitorBase + { + protected: + bool HasLock(const EntityAliasList* aliases) const; + bool HasAliases(const EntityAliasList* aliases) const; + bool AreAllSpawnablesReady(const EntityAliasList* aliases) const; + + EntityAliasList::const_iterator begin(const EntityAliasList* aliases) const; + EntityAliasList::const_iterator end(const EntityAliasList* aliases) const; + EntityAliasList::const_iterator cbegin(const EntityAliasList* aliases) const; + EntityAliasList::const_iterator cend(const EntityAliasList* aliases) const; + + using ListTargetSpawanblesCallback = AZStd::function& targetSpawnable)>; + void ListTargetSpawnables(const EntityAliasList* aliases, const ListTargetSpawanblesCallback& callback) const; + void ListTargetSpawnables(const EntityAliasList* aliases, AZ::Crc32 tag, const ListTargetSpawanblesCallback& callback) const; + }; + + public: + class EntityAliasVisitor final : public EntityAliasVisitorBase + { + public: + EntityAliasVisitor(Spawnable& owner, EntityAliasList* m_entityAliasList); + ~EntityAliasVisitor(); + + EntityAliasVisitor(EntityAliasVisitor&& rhs); + EntityAliasVisitor& operator=(EntityAliasVisitor&& rhs); + + EntityAliasVisitor(const EntityAliasVisitor& rhs) = delete; + EntityAliasVisitor& operator=(const EntityAliasVisitor& rhs) = delete; + + bool HasLock() const; + bool HasAliases() const; + bool AreAllSpawnablesReady() const; + + EntityAliasList::const_iterator begin() const; + EntityAliasList::const_iterator end() const; + EntityAliasList::const_iterator cbegin() const; + EntityAliasList::const_iterator cend() const; + + void ListTargetSpawnables(const ListTargetSpawanblesCallback& callback) const; + void ListTargetSpawnables(AZ::Crc32 tag, const ListTargetSpawanblesCallback& callback) const; + + void AddAlias( + AZ::Data::Asset targetSpawnable, + AZ::Crc32 tag, + uint32_t sourceIndex, + uint32_t targetIndex, + Spawnable::EntityAliasType aliasType, + bool queueLoad); + + using ListSpawnablesPendingLoadCallback = AZStd::function& spawnablePendingLoad)>; + void ListSpawnablesPendingLoad(const ListSpawnablesPendingLoadCallback& callback); + + using UpdateCallback = AZStd::function& aliasedSpawnable, + const AZ::Crc32 tag, + const uint32_t sourceIndex, + const uint32_t targetIndex)>; + void UpdateAliases(const UpdateCallback& callback); + void UpdateAliases(AZ::Crc32 tag, const UpdateCallback& callback); + void UpdateAliasType(uint32_t index, Spawnable::EntityAliasType newType); + + void Optimize(); + + private: + Spawnable& m_owner; + EntityAliasList* m_entityAliasList{ nullptr }; + bool m_dirty{ false }; + }; + + class EntityAliasConstVisitor final : public EntityAliasVisitorBase + { + public: + EntityAliasConstVisitor(const Spawnable& owner, const EntityAliasList* m_entityAliasList); + ~EntityAliasConstVisitor(); + + bool HasLock() const; + bool HasAliases() const; + bool AreAllSpawnablesReady() const; + + EntityAliasList::const_iterator begin() const; + EntityAliasList::const_iterator end() const; + EntityAliasList::const_iterator cbegin() const; + EntityAliasList::const_iterator cend() const; + + void ListTargetSpawnables(const ListTargetSpawanblesCallback& callback) const; + void ListTargetSpawnables(AZ::Crc32 tag, const ListTargetSpawanblesCallback& callback) const; + + private: + const Spawnable& m_owner; + const EntityAliasList* m_entityAliasList; + + }; inline static constexpr const char* FileExtension = "spawnable"; inline static constexpr const char* DotFileExtension = ".spawnable"; @@ -39,14 +172,24 @@ namespace AzFramework Spawnable(const Spawnable& rhs) = delete; Spawnable(Spawnable&& other) = delete; ~Spawnable() override = default; - + Spawnable& operator=(const Spawnable& rhs) = delete; Spawnable& operator=(Spawnable&& other) = delete; const EntityList& GetEntities() const; EntityList& GetEntities(); + EntityAliasConstVisitor TryGetAliasesConst() const; + EntityAliasConstVisitor TryGetAliases() const; + EntityAliasVisitor TryGetAliases(); bool IsEmpty() const; + //! Whether or not the spawnable is permanently locked. If so then parts of the spawnable can no longer be modified. + bool IsPermanentlyLocked() const; + //! Permanently locks access to parts of the spawnable from being modified. + //! @return True if the spawnable could be locked. If false is returned another operation is still making modifications. In this case + //! call this again at a later point in time. + bool LockPermanently(); + SpawnableMetaData& GetMetaData(); const SpawnableMetaData& GetMetaData() const; @@ -55,11 +198,12 @@ namespace AzFramework private: SpawnableMetaData m_metaData; + // Aliases that optionally replace the ones stored in this spawnable. + EntityAliasList m_entityAliases; // Container for keeping all entities of the prefab the Spawnable was created from. // Includes both direct and nested entities of the prefab. EntityList m_entities; + + mutable AZStd::atomic m_lockState{ LockState::Unlocked }; }; - - using SpawnableList = AZStd::vector; - } // namespace AzFramework diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableAssetBus.h b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableAssetBus.h new file mode 100644 index 0000000000..d3d7bfabb7 --- /dev/null +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableAssetBus.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include +#include + +namespace AzFramework +{ + class SpawnableAssetEvents : public AZ::EBusTraits + { + public: + using MutexType = AZStd::recursive_mutex; + + //! Callback to allow the entity aliases in a spawnable to adjusted based on runtime requirements. + //! This will be called by the Asset Manager as part of the creation of the spawnable asset from loaded file data. Any work done + //! in this callback will be counted towards the maximum amount of time allocated to asset handlers to construct their assets, + //! it's recommended to keep work done in this callback to a minimum and prefer delaying any complex processing. + //! + //! ALERT: Do not start blocking asset requests in this callback. + //! Since this is part of the Asset Manager's asset streaming, doing a blocking load in this callback will cause the job + //! processing the spawnable asset to locked out of doing any asset streaming work. If there are more spawnables doing + //! this than there are job threads available the engine will enter a deadlock situation as no more assets can complete + //! loading and no job threads become free as they're all waiting for assets to complete. It is however safe to queue + //! an asset for loading. + virtual void OnResolveAliases( + Spawnable::EntityAliasVisitor& aliases, const SpawnableMetaData& metadata, const Spawnable::EntityList& entities) = 0; + }; + + using SpawnableAssetEventsBus = AZ::EBus; +} // namespace AzFramework diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableAssetHandler.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableAssetHandler.cpp index c24b538de7..bf4d350a69 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableAssetHandler.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableAssetHandler.cpp @@ -9,8 +9,10 @@ #include #include #include +#include #include #include +#include namespace AzFramework { @@ -52,6 +54,7 @@ namespace AzFramework AZ::ObjectStream::FilterDescriptor filter(assetLoadFilterCB); if (AZ::Utils::LoadObjectFromStreamInPlace(*stream, *spawnable, nullptr /*SerializeContext*/, filter)) { + ResolveEntityAliases(spawnable, asset, stream->GetStreamingDeadline(), stream->GetStreamingPriority(), assetLoadFilterCB); return AZ::Data::AssetHandler::LoadResult::LoadComplete; } else @@ -91,4 +94,40 @@ namespace AzFramework AZ::Uuid subIdHash = AZ::Uuid::CreateData(id.data(), id.size()); return azlossy_caster(subIdHash.GetHash()); } + + void SpawnableAssetHandler::ResolveEntityAliases( + Spawnable* spawnable, + const AZ::Data::Asset& asset, + AZStd::chrono::milliseconds streamingDeadline, + AZ::IO::IStreamerTypes::Priority streamingPriority, + const AZ::Data::AssetFilterCB& assetLoadFilterCB) + { + Spawnable::EntityAliasVisitor aliases = spawnable->TryGetAliases(); + AZ_Assert(aliases.HasLock(), "Newly created Spawnable '%s' was already locked.", asset.GetHint().c_str()); + if (aliases.HasAliases()) + { + AZ_Assert( + AZStd::is_sorted( + aliases.begin(), aliases.end(), + [](const Spawnable::EntityAlias& lhs, const Spawnable::EntityAlias& rhs) + { + return lhs.HasLowerIndex(rhs); + }), + "Spawnable '%s' has an unsorted entity alias list.", asset.GetHint().c_str()); + + SpawnableAssetEventsBus::Broadcast( + &SpawnableAssetEvents::OnResolveAliases, aliases, spawnable->GetMetaData(), spawnable->GetEntities()); + + aliases.Optimize(); + aliases.ListSpawnablesPendingLoad( + [&assetLoadFilterCB, streamingDeadline, streamingPriority](AZ::Data::Asset& assetPendingLoad) + { + AZ::Data::AssetLoadParameters loadInfo; + loadInfo.m_assetLoadFilterCB = assetLoadFilterCB; + loadInfo.m_deadline = streamingDeadline; + loadInfo.m_priority = streamingPriority; + assetPendingLoad.QueueLoad(loadInfo); + }); + } + } } // namespace AzFramework diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableAssetHandler.h b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableAssetHandler.h index 94ec9b13fd..e043019e29 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableAssetHandler.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableAssetHandler.h @@ -50,5 +50,13 @@ namespace AzFramework const AZ::Data::Asset& asset, AZStd::shared_ptr stream, const AZ::Data::AssetFilterCB& assetLoadFilterCB) override; + + private: + void ResolveEntityAliases( + class Spawnable* spawnable, + const AZ::Data::Asset& asset, + AZStd::chrono::milliseconds streamingDeadline, + AZ::IO::IStreamerTypes::Priority streamingPriority, + const AZ::Data::AssetFilterCB& assetLoadFilterCB); }; } // namespace AzFramework diff --git a/Code/Framework/AzFramework/AzFramework/azframework_files.cmake b/Code/Framework/AzFramework/AzFramework/azframework_files.cmake index e03d166cfc..22fbddb39a 100644 --- a/Code/Framework/AzFramework/AzFramework/azframework_files.cmake +++ b/Code/Framework/AzFramework/AzFramework/azframework_files.cmake @@ -286,6 +286,7 @@ set(FILES Spawnable/RootSpawnableInterface.h Spawnable/Spawnable.cpp Spawnable/Spawnable.h + Spawnable/SpawnableAssetBus.h Spawnable/SpawnableAssetHandler.h Spawnable/SpawnableAssetHandler.cpp Spawnable/SpawnableEntitiesContainer.h From a05d5f5d6dbc18ed85654b853fe9b815ebd70b8e Mon Sep 17 00:00:00 2001 From: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> Date: Mon, 25 Oct 2021 14:35:49 -0700 Subject: [PATCH 03/14] Extended the Spawnable Entities Interface to allow entity aliases to be updated. Entity aliases can now be updated as a reaction to the spawnable being loaded or at any other time afterwards through the Spawnable Entities Interface. Currently these changes are applied to the spawnable that owns the entity aliases, but once the Spawnable Entities Interface makes use of AzFramework::Scene a copy of the entity aliases should be stored in the scene and be updated instead of the spawnable. This change also adds support for a load barrier, which acts the same as a regular barrier but also accounts for the spawnable being loaded and won't trigger the callback until has completed. The return values in from the processing functions in the Spawnable Entities Manager now have a clearer return value to indicate whether a request has completed or is being re-queued. Signed-off-by: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> --- .../AzFramework/Spawnable/Spawnable.cpp | 9 +- .../Spawnable/SpawnableEntitiesContainer.cpp | 24 +- .../Spawnable/SpawnableEntitiesContainer.h | 16 +- .../Spawnable/SpawnableEntitiesInterface.cpp | 8 +- .../Spawnable/SpawnableEntitiesInterface.h | 64 +- .../Spawnable/SpawnableEntitiesManager.cpp | 602 +++++++++++++----- .../Spawnable/SpawnableEntitiesManager.h | 90 ++- 7 files changed, 595 insertions(+), 218 deletions(-) diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp index 2be76c28a6..92728a4575 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp @@ -46,8 +46,13 @@ namespace AzFramework AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked."); for (const EntityAlias& alias : *aliases) { - if ((alias.m_aliasType != Spawnable::EntityAliasType::Original && alias.m_aliasType != Spawnable::EntityAliasType::Disabled) && - !alias.m_spawnable.IsReady()) + if (!alias.m_queueLoad || + alias.m_aliasType == Spawnable::EntityAliasType::Original || + alias.m_aliasType == Spawnable::EntityAliasType::Disabled) + { + continue; + } + if (!alias.m_spawnable.IsReady() && !alias.m_spawnable.IsError()) { return false; } diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesContainer.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesContainer.cpp index b98ea275e4..912bf05058 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesContainer.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesContainer.cpp @@ -26,7 +26,7 @@ namespace AzFramework return m_threadData != nullptr; } - uint64_t SpawnableEntitiesContainer::GetCurrentGeneration() const + uint32_t SpawnableEntitiesContainer::GetCurrentGeneration() const { return m_currentGeneration; } @@ -37,7 +37,7 @@ namespace AzFramework SpawnableEntitiesInterface::Get()->SpawnAllEntities(m_threadData->m_spawnedEntitiesTicket); } - void SpawnableEntitiesContainer::SpawnEntities(AZStd::vector entityIndices) + void SpawnableEntitiesContainer::SpawnEntities(AZStd::vector entityIndices) { AZ_Assert(m_threadData, "Calling SpawnEntities on a Spawnable container that's not set."); SpawnableEntitiesInterface::Get()->SpawnEntities( @@ -78,15 +78,21 @@ namespace AzFramework } } - void SpawnableEntitiesContainer::Alert(AlertCallback callback) + void SpawnableEntitiesContainer::Alert(AlertCallback callback, CheckIfSpawnableIsLoaded spawnableCheck) { AZ_Assert(m_threadData, "Calling DespawnEntities on a Spawnable container that's not set."); - SpawnableEntitiesInterface::Get()->Barrier( - m_threadData->m_spawnedEntitiesTicket, - [generation = m_threadData->m_generation, callback = AZStd::move(callback)](EntitySpawnTicket::Id) - { - callback(generation); - }); + auto callbackWrapper = [generation = m_threadData->m_generation, callback = AZStd::move(callback)](EntitySpawnTicket::Id) + { + callback(generation); + }; + if (spawnableCheck == CheckIfSpawnableIsLoaded::No) + { + SpawnableEntitiesInterface::Get()->Barrier(m_threadData->m_spawnedEntitiesTicket, AZStd::move(callbackWrapper)); + } + else + { + SpawnableEntitiesInterface::Get()->LoadBarrier(m_threadData->m_spawnedEntitiesTicket, AZStd::move(callbackWrapper)); + } } void SpawnableEntitiesContainer::Connect(AZ::Data::Asset spawnable) diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesContainer.h b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesContainer.h index 6fa295e18c..1ec6e6a665 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesContainer.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesContainer.h @@ -36,6 +36,12 @@ namespace AzFramework public: using AlertCallback = AZStd::function; + enum class CheckIfSpawnableIsLoaded : bool + { + Yes, + No + }; + //! Constructs a new spawnables entity container that has not been connected. SpawnableEntitiesContainer() = default; //! Constructs a new spawnables entity container that connects to the provided spawnable. @@ -48,13 +54,13 @@ namespace AzFramework //! Returns a number that identifies the current generation of the container with. The completion callback can still receive //! calls from older generations as processing completes on those. The returned value can be used to help calls tell //! older versions apart from newer ones. - [[nodiscard]] uint64_t GetCurrentGeneration() const; + [[nodiscard]] uint32_t GetCurrentGeneration() const; //! Puts in a request to spawn entities using all entities in the provided spawnable as a template. void SpawnAllEntities(); //! Puts in a request to spawn entities using the entities found in the spawnable at the provided indices as a template. //! @param entityIndices A list of indices to the entities in the spawnable. - void SpawnEntities(AZStd::vector entityIndices); + void SpawnEntities(AZStd::vector entityIndices); //! Puts in a request to despawn all previous spawned entities. void DespawnAllEntities(); @@ -73,7 +79,11 @@ namespace AzFramework //! other than the calling thread including the main thread. Note that because the alert is queued it can still be called //! after the container has been deleted or can be called for a previously assigned spawnable. In the latter case check //! if the current generation matches the generation provided with the callback. - void Alert(AlertCallback callback); + //! @callback The function called when the alert triggers. This can be called from a different thread than the one that + //! the one that made the call to Alert. + //! @checkSpawnableIsLoaded If true the alert will also block until the spawnable has been loaded. If false then it will + //! be called after all previous calls have completed, but the spawnable may not be loaded at that point. + void Alert(AlertCallback callback, CheckIfSpawnableIsLoaded spawnableCheck = CheckIfSpawnableIsLoaded::No); private: void Connect(AZ::Data::Asset spawnable); diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.cpp index 171d626b27..37091d8f0f 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.cpp @@ -152,7 +152,7 @@ namespace AzFramework // SpawnableIndexEntityPair // - SpawnableIndexEntityPair::SpawnableIndexEntityPair(AZ::Entity** entityIterator, size_t* indexIterator) + SpawnableIndexEntityPair::SpawnableIndexEntityPair(AZ::Entity** entityIterator, uint32_t* indexIterator) : m_entity(entityIterator) , m_index(indexIterator) { @@ -168,7 +168,7 @@ namespace AzFramework return *m_entity; } - size_t SpawnableIndexEntityPair::GetIndex() const + uint32_t SpawnableIndexEntityPair::GetIndex() const { return *m_index; } @@ -177,7 +177,7 @@ namespace AzFramework // SpawnableIndexEntityIterator // - SpawnableIndexEntityIterator::SpawnableIndexEntityIterator(AZ::Entity** entityIterator, size_t* indexIterator) + SpawnableIndexEntityIterator::SpawnableIndexEntityIterator(AZ::Entity** entityIterator, uint32_t* indexIterator) : m_value(entityIterator, indexIterator) { } @@ -248,7 +248,7 @@ namespace AzFramework // SpawnableConstIndexEntityContainerView::SpawnableConstIndexEntityContainerView( - AZ::Entity** beginEntity, size_t* beginIndices, size_t length) + AZ::Entity** beginEntity, uint32_t* beginIndices, size_t length) : m_begin(beginEntity, beginIndices) , m_end(beginEntity + length, beginIndices + length) { diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h index 74a17020df..dc9c7b4538 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h @@ -85,19 +85,19 @@ namespace AzFramework AZ::Entity* GetEntity(); const AZ::Entity* GetEntity() const; - size_t GetIndex() const; + uint32_t GetIndex() const; private: SpawnableIndexEntityPair() = default; SpawnableIndexEntityPair(const SpawnableIndexEntityPair&) = default; SpawnableIndexEntityPair(SpawnableIndexEntityPair&&) = default; - SpawnableIndexEntityPair(AZ::Entity** entityIterator, size_t* indexIterator); + SpawnableIndexEntityPair(AZ::Entity** entityIterator, uint32_t* indexIterator); SpawnableIndexEntityPair& operator=(const SpawnableIndexEntityPair&) = default; SpawnableIndexEntityPair& operator=(SpawnableIndexEntityPair&&) = default; AZ::Entity** m_entity { nullptr }; - size_t* m_index { nullptr }; + uint32_t* m_index { nullptr }; }; class SpawnableIndexEntityIterator @@ -110,7 +110,7 @@ namespace AzFramework using pointer = SpawnableIndexEntityPair*; using reference = SpawnableIndexEntityPair&; - SpawnableIndexEntityIterator(AZ::Entity** entityIterator, size_t* indexIterator); + SpawnableIndexEntityIterator(AZ::Entity** entityIterator, uint32_t* indexIterator); SpawnableIndexEntityIterator& operator++(); SpawnableIndexEntityIterator operator++(int); @@ -132,7 +132,7 @@ namespace AzFramework class SpawnableConstIndexEntityContainerView { public: - SpawnableConstIndexEntityContainerView(AZ::Entity** beginEntity, size_t* beginIndices, size_t length); + SpawnableConstIndexEntityContainerView(AZ::Entity** beginEntity, uint32_t* beginIndices, size_t length); const SpawnableIndexEntityIterator& begin(); const SpawnableIndexEntityIterator& end(); @@ -144,6 +144,16 @@ namespace AzFramework SpawnableIndexEntityIterator m_end; }; + //! Information used when updating the type of an entity alias. + struct EntityAliasTypeChange + { + //! The index of the alias in the spawnable. Note that due to optimizations done on the entity aliases the index of an alias + //! can change over time. + uint32_t m_aliasIndex; + //! The type to replace type stored in the spawnable at the index provided by m_aliasIndex. + Spawnable::EntityAliasType m_newAliasType; + }; + //! Requests to the SpawnableEntitiesInterface require a ticket with a valid spawnable that is used as a template. A ticket can //! be reused for multiple calls on the same spawnable and is safe to be used by multiple threads at the same time. Entities created //! from the spawnable may be tracked by the ticket and so using the same ticket is needed to despawn the exact entities created @@ -178,6 +188,7 @@ namespace AzFramework using EntityDespawnCallback = AZStd::function; using RetrieveEntitySpawnTicketCallback = AZStd::function; using ReloadSpawnableCallback = AZStd::function; + using UpdateEntityAliasTypesCallback = AZStd::function; using ListEntitiesCallback = AZStd::function; using ListIndicesEntitiesCallback = AZStd::function; using ClaimEntitiesCallback = AZStd::function; @@ -247,6 +258,15 @@ namespace AzFramework SpawnablePriority m_priority { SpawnablePriority_Default }; }; + struct UpdateEntityAliasTypesOptionalArgs final + { + //! Callback that's called when entity aliases are updated. This can be triggered from a different thread than the one that + //! made the function call to update. + UpdateEntityAliasTypesCallback m_completionCallback; + //! The priority at which this call will be executed. + SpawnablePriority m_priority{ SpawnablePriority_Default }; + }; + struct ListEntitiesOptionalArgs final { //! The priority at which this call will be executed. @@ -265,6 +285,14 @@ namespace AzFramework SpawnablePriority m_priority{ SpawnablePriority_Default }; }; + struct LoadBarrierOptionalArgs final + { + //! The priority at which this call will be executed. + SpawnablePriority m_priority{ SpawnablePriority_Default }; + //! Also checks if the spawnables referenced in the entity aliases that are marked to be loaded are loaded. + bool m_checkAliasSpawnables{ true }; + }; + //! Interface definition to (de)spawn entities from a spawnable into the game world. //! //! While the callbacks of the individual calls are being processed they will block processing any other request. Callbacks can be @@ -298,7 +326,7 @@ namespace AzFramework //! @param entityIndices The indices into the template entities stored in the spawnable that will be used to spawn entities from. //! @param optionalArgs Optional additional arguments, see SpawnEntitiesOptionalArgs. virtual void SpawnEntities( - EntitySpawnTicket& ticket, AZStd::vector entityIndices, SpawnEntitiesOptionalArgs optionalArgs = {}) = 0; + EntitySpawnTicket& ticket, AZStd::vector entityIndices, SpawnEntitiesOptionalArgs optionalArgs = {}) = 0; //! Removes all entities in the provided list from the environment. //! @param ticket The ticket previously used to spawn entities with. //! @param optionalArgs Optional additional arguments, see DespawnAllEntitiesOptionalArgs. @@ -320,6 +348,16 @@ namespace AzFramework virtual void ReloadSpawnable( EntitySpawnTicket& ticket, AZ::Data::Asset spawnable, ReloadSpawnableOptionalArgs optionalArgs = {}) = 0; + //! Allows updating the entity alias on a spawnable. This allows the spawning behavior for all entities spawned from the used + //! spawnable to be changed and is not restricted to this ticket alone. + //! @param ticket Holds the information for the spawnable. + //! @param updateAliases An array of index and alias type values used to update the entity alias list. + //! @param optionalArgs Optional additional arguments, see UpdateEntityAliasTypesOptionalArgs. + virtual void UpdateEntityAliasTypes( + EntitySpawnTicket& ticket, + AZStd::vector updatedAliases, + UpdateEntityAliasTypesOptionalArgs optionalArgs = {}) = 0; + //! List all entities that are spawned using this ticket. //! @param ticket Only the entities associated with this ticket will be listed. //! @param listCallback Required callback that will be called to list the entities on. @@ -351,31 +389,37 @@ namespace AzFramework //! @param completionCallback Required callback that will be called as soon as the barrier has been reached. //! @param optionalArgs Optional additional arguments, see BarrierOptionalArgs. virtual void Barrier(EntitySpawnTicket& ticket, BarrierCallback completionCallback, BarrierOptionalArgs optionalArgs = {}) = 0; + //! Blocks until the spawnable is loaded and all operations made on the provided ticket before the barrier call have completed. + //! @param ticket The ticket to monitor. + //! @param completionCallback Required callback that will be called as soon as the barrier has been reached. + //! @param optionalArgs Optional additional arguments, see BarrierOptionalArgs. + virtual void LoadBarrier( + EntitySpawnTicket& ticket, BarrierCallback completionCallback, LoadBarrierOptionalArgs optionalArgs = {}) = 0; protected: [[nodiscard]] virtual AZStd::pair CreateTicket(AZ::Data::Asset&& spawnable) = 0; virtual void DestroyTicket(void* ticket) = 0; template - static T& GetTicketPayload(EntitySpawnTicket& ticket) + [[nodiscard]] static T& GetTicketPayload(EntitySpawnTicket& ticket) { return *reinterpret_cast(ticket.m_payload); } template - static const T& GetTicketPayload(const EntitySpawnTicket& ticket) + [[nodiscard]] static const T& GetTicketPayload(const EntitySpawnTicket& ticket) { return *reinterpret_cast(ticket.m_payload); } template - static T* GetTicketPayload(EntitySpawnTicket* ticket) + [[nodiscard]] static T* GetTicketPayload(EntitySpawnTicket* ticket) { return reinterpret_cast(ticket->m_payload); } template - static const T* GetTicketPayload(const EntitySpawnTicket* ticket) + [[nodiscard]] static const T* GetTicketPayload(const EntitySpawnTicket* ticket) { return reinterpret_cast(ticket->m_payload); } diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp index ef7351aabb..0ac80c3ed6 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp @@ -60,7 +60,7 @@ namespace AzFramework } void SpawnableEntitiesManager::SpawnEntities( - EntitySpawnTicket& ticket, AZStd::vector entityIndices, SpawnEntitiesOptionalArgs optionalArgs) + EntitySpawnTicket& ticket, AZStd::vector entityIndices, SpawnEntitiesOptionalArgs optionalArgs) { AZ_Assert(ticket.IsValid(), "Ticket provided to SpawnEntities hasn't been initialized."); @@ -128,6 +128,20 @@ namespace AzFramework QueueRequest(ticket, optionalArgs.m_priority, AZStd::move(queueEntry)); } + void SpawnableEntitiesManager::UpdateEntityAliasTypes( + EntitySpawnTicket& ticket, + AZStd::vector updatedAliases, + UpdateEntityAliasTypesOptionalArgs optionalArgs) + { + AZ_Assert(ticket.IsValid(), "Ticket provided to ReloadSpawnable hasn't been initialized."); + + UpdateEntityAliasTypesCommand queueEntry; + queueEntry.m_entityAliases = AZStd::move(updatedAliases); + queueEntry.m_ticketId = ticket.GetId(); + queueEntry.m_completionCallback = AZStd::move(optionalArgs.m_completionCallback); + QueueRequest(ticket, optionalArgs.m_priority, AZStd::move(queueEntry)); + } + void SpawnableEntitiesManager::ListEntities( EntitySpawnTicket& ticket, ListEntitiesCallback listCallback, ListEntitiesOptionalArgs optionalArgs) { @@ -175,6 +189,19 @@ namespace AzFramework QueueRequest(ticket, optionalArgs.m_priority, AZStd::move(queueEntry)); } + void SpawnableEntitiesManager::LoadBarrier( + EntitySpawnTicket& ticket, BarrierCallback completionCallback, LoadBarrierOptionalArgs optionalArgs) + { + AZ_Assert(completionCallback, "Load barrier on spawnable entities called without a valid callback to use."); + AZ_Assert(ticket.IsValid(), "Ticket provided to LoadBarrier hasn't been initialized."); + + LoadBarrierCommand queueEntry; + queueEntry.m_ticketId = ticket.GetId(); + queueEntry.m_completionCallback = AZStd::move(completionCallback); + queueEntry.m_checkAliasSpawnables = optionalArgs.m_checkAliasSpawnables; + QueueRequest(ticket, optionalArgs.m_priority, AZStd::move(queueEntry)); + } + auto SpawnableEntitiesManager::ProcessQueue(CommandQueuePriority priority) -> CommandQueueStatus { CommandQueueStatus result = CommandQueueStatus::NoCommandsLeft; @@ -203,13 +230,13 @@ namespace AzFramework for (size_t i = 0; i < delayedSize; ++i) { Requests& request = queue.m_delayed.front(); - bool result = AZStd::visit( - [this](auto&& args) -> bool + CommandResult result = AZStd::visit( + [this](auto&& args) -> CommandResult { return ProcessRequest(args); }, request); - if (!result) + if (result == CommandResult::Requeue) { queue.m_delayed.emplace_back(AZStd::move(request)); } @@ -230,13 +257,13 @@ namespace AzFramework while (!pendingRequestQueue.empty()) { Requests& request = pendingRequestQueue.front(); - bool result = AZStd::visit( - [this](auto&& args) -> bool + CommandResult result = AZStd::visit( + [this](auto&& args) -> CommandResult { return ProcessRequest(args); }, request); - if (!result) + if (result == CommandResult::Requeue) { queue.m_delayed.emplace_back(AZStd::move(request)); } @@ -276,11 +303,81 @@ namespace AzFramework AZ::Entity* SpawnableEntitiesManager::CloneSingleEntity(const AZ::Entity& entityTemplate, EntityIdMap& templateToCloneMap, AZ::SerializeContext& serializeContext) { - // If the same ID gets remapped more than once, preserve the original remapping instead of overwriting it. + if (!entityTemplate.GetComponents().empty()) + { + // If the same ID gets remapped more than once, preserve the original remapping instead of overwriting it. + constexpr bool allowDuplicateIds = false; + + return AZ::IdUtils::Remapper::CloneObjectAndGenerateNewIdsAndFixRefs( + &entityTemplate, templateToCloneMap, &serializeContext); + } + else + { + return nullptr; + } + } + + AZ::Entity* SpawnableEntitiesManager::CloneSingleAliasedEntity( + const AZ::Entity& entityTemplate, + const Spawnable::EntityAlias& alias, + EntityIdMap& templateToCloneMap, + AZ::Entity* previouslySpawnedEntity, + AZ::SerializeContext& serializeContext) + { + using ResultType = AZStd::pair; + + AZ::Entity* clone = nullptr; + switch (alias.m_aliasType) + { + case Spawnable::EntityAliasType::Original: + // Behave as the original version. + clone = CloneSingleEntity(entityTemplate, templateToCloneMap, serializeContext); + AZ_Assert(clone != nullptr, "Failed to clone spawnable entity."); + return clone; + case Spawnable::EntityAliasType::Disabled: + // Do nothing. + return nullptr; + case Spawnable::EntityAliasType::Replace: + clone = CloneSingleEntity(*(alias.m_spawnable->GetEntities()[alias.m_targetIndex]), templateToCloneMap, serializeContext); + AZ_Assert(clone != nullptr, "Failed to clone spawnable entity."); + return clone; + case Spawnable::EntityAliasType::Additional: + // The asset handler will have sorted and inserted a Spawnable::EntityAliasType::Original, so the just + // spawn the additional entity. + clone = CloneSingleEntity(*(alias.m_spawnable->GetEntities()[alias.m_targetIndex]), templateToCloneMap, serializeContext); + AZ_Assert(clone != nullptr, "Failed to clone spawnable entity."); + return clone; + case Spawnable::EntityAliasType::Merge: + AZ_Assert(previouslySpawnedEntity != nullptr, "Merging components but there's no entity to add to yet."); + AZ_Assert( + previouslySpawnedEntity->GetId() == alias.m_spawnable->GetEntities()[alias.m_targetIndex]->GetId(), + "Entity ids for merging spawnables don't match."); + AppendComponents( + *previouslySpawnedEntity, alias.m_spawnable->GetEntities()[alias.m_targetIndex]->GetComponents(), templateToCloneMap, serializeContext); + return nullptr; + default: + AZ_Assert(false, "Unsupported spawnable entity alias type: %i", alias.m_aliasType); + return nullptr; + } + } + + void SpawnableEntitiesManager::AppendComponents( + AZ::Entity& target, + const AZ::Entity::ComponentArrayType& componentTemplates, + EntityIdMap& templateToCloneMap, + AZ::SerializeContext& serializeContext) + { + // Only components are added and entities are looked up so no duplicate entity ids should be encountered. constexpr bool allowDuplicateIds = false; - return AZ::IdUtils::Remapper::CloneObjectAndGenerateNewIdsAndFixRefs( - &entityTemplate, templateToCloneMap, &serializeContext); + for (const AZ::Component* component : componentTemplates) + { + AZ::Component* clone = AZ::IdUtils::Remapper::CloneObjectAndGenerateNewIdsAndFixRefs( + component, templateToCloneMap, &serializeContext); + AZ_Assert(clone, "Unable to clone component for entity '%s' (%zu).", target.GetName().c_str(), target.GetId()); + [[maybe_unused]] bool result = target.AddComponent(clone); + AZ_Assert(result, "Unable to add cloned component to entity '%s' (%zu).", target.GetName().c_str(), target.GetId()); + } } void SpawnableEntitiesManager::InitializeEntityIdMappings( @@ -316,161 +413,276 @@ namespace AzFramework } } - - bool SpawnableEntitiesManager::ProcessRequest(SpawnAllEntitiesCommand& request) + auto SpawnableEntitiesManager::ProcessRequest(SpawnAllEntitiesCommand& request) -> CommandResult { Ticket& ticket = *request.m_ticket; if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId) { - AZStd::vector& spawnedEntities = ticket.m_spawnedEntities; - AZStd::vector& spawnedEntityIndices = ticket.m_spawnedEntityIndices; - - // Keep track 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 = entitiesToSpawn.size(); - - // Reserve buffers - spawnedEntities.reserve(spawnedEntities.size() + entitiesToSpawnSize); - spawnedEntityIndices.reserve(spawnedEntityIndices.size() + entitiesToSpawnSize); - - // Pre-generate the full set of entity id to new entity id mappings, so that during the clone operation below, - // any entity references that point to a not-yet-cloned entity will still get their ids remapped correctly. - // We clear out and regenerate the set of IDs on every SpawnAllEntities call, because presumably every entity reference - // in every entity we're about to instantiate is intended to point to an entity in our newly-instantiated batch, regardless - // of spawn order. If we didn't clear out the map, it would be possible for some entities here to have references to - // previously-spawned entities from a previous SpawnEntities or SpawnAllEntities call. - InitializeEntityIdMappings(entitiesToSpawn, ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned); - - for (size_t i = 0; i < entitiesToSpawnSize; ++i) + if (Spawnable::EntityAliasConstVisitor aliases = ticket.m_spawnable->TryGetAliasesConst(); + aliases.HasLock() && aliases.AreAllSpawnablesReady()) { - // If this entity has previously been spawned, give it a new id in the reference map - RefreshEntityIdMapping(entitiesToSpawn[i].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned); + AZStd::vector& spawnedEntities = ticket.m_spawnedEntities; + AZStd::vector& spawnedEntityIndices = ticket.m_spawnedEntityIndices; - AZ::Entity* clone = CloneSingleEntity(*entitiesToSpawn[i], ticket.m_entityIdReferenceMap, *request.m_serializeContext); - AZ_Assert(clone != nullptr, "Failed to clone spawnable entity."); + // Keep track how many entities there were in the array initially + size_t spawnedEntitiesInitialCount = spawnedEntities.size(); - spawnedEntities.emplace_back(clone); - spawnedEntityIndices.push_back(i); - } + // These are 'template' entities we'll be cloning from + const Spawnable::EntityList& entitiesToSpawn = ticket.m_spawnable->GetEntities(); + uint32_t entitiesToSpawnSize = aznumeric_caster(entitiesToSpawn.size()); - // loadAll is true if every entity has been spawned only once - 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) - { - request.m_preInsertionCallback(request.m_ticketId, SpawnableEntityContainerView( - ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end())); - } + // Reserve buffers + spawnedEntities.reserve(spawnedEntities.size() + entitiesToSpawnSize); + spawnedEntityIndices.reserve(spawnedEntityIndices.size() + entitiesToSpawnSize); - // Add to the game context, now the entities are active - for (auto it = ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount; it != ticket.m_spawnedEntities.end(); ++it) - { - (*it)->SetSpawnTicketId(request.m_ticketId); - 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) - { - request.m_completionCallback(request.m_ticketId, SpawnableConstEntityContainerView( - ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end())); - } - - ticket.m_currentRequestId++; - return true; - } - else - { - return false; - } - } - - bool SpawnableEntitiesManager::ProcessRequest(SpawnEntitiesCommand& request) - { - Ticket& ticket = *request.m_ticket; - if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId) - { - 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 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(); - - if (ticket.m_entityIdReferenceMap.empty() || !request.m_referencePreviouslySpawnedEntities) - { - // This map keeps track of ids from template (spawnable) to clone (instance) allowing patch ups of fields referring - // to entityIds outside of a given entity. - // We pre-generate the full set of entity id to new entity id mappings, so that during the clone operation below, + // Pre-generate the full set of entity-id-to-new-entity-id mappings, so that during the clone operation below, // any entity references that point to a not-yet-cloned entity will still get their ids remapped correctly. - // By default, we only initialize this map once because it needs to persist across multiple SpawnEntities calls, so - // that reference fixups work even when the entity being referenced is spawned in a different SpawnEntities - // (or SpawnAllEntities) call. - // However, the caller can also choose to reset the map by passing in "m_referencePreviouslySpawnedEntities = false". + // We clear out and regenerate the set of IDs on every SpawnAllEntities call, because presumably every entity reference + // in every entity we're about to instantiate is intended to point to an entity in our newly-instantiated batch, regardless + // of spawn order. If we didn't clear out the map, it would be possible for some entities here to have references to + // previously-spawned entities from a previous SpawnEntities or SpawnAllEntities call. InitializeEntityIdMappings(entitiesToSpawn, ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned); - } - spawnedEntities.reserve(spawnedEntities.size() + entitiesToSpawnSize); - spawnedEntityIndices.reserve(spawnedEntityIndices.size() + entitiesToSpawnSize); - - for (size_t index : request.m_entityIndices) - { - if (index < entitiesToSpawn.size()) + auto aliasIt = aliases.begin(); + auto aliasEnd = aliases.end(); + if (aliasIt == aliasEnd) { - // If this entity has previously been spawned, give it a new id in the reference map - RefreshEntityIdMapping( - entitiesToSpawn[index].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned); + for (uint32_t i = 0; i < entitiesToSpawnSize; ++i) + { + // If this entity has previously been spawned, give it a new id in the reference map + RefreshEntityIdMapping( + entitiesToSpawn[i].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned); - AZ::Entity* clone = - CloneSingleEntity(*entitiesToSpawn[index], ticket.m_entityIdReferenceMap, *request.m_serializeContext); - AZ_Assert(clone != nullptr, "Failed to clone spawnable entity."); - - spawnedEntities.push_back(clone); - spawnedEntityIndices.push_back(index); + spawnedEntities.emplace_back( + CloneSingleEntity(*entitiesToSpawn[i], ticket.m_entityIdReferenceMap, *request.m_serializeContext)); + spawnedEntityIndices.push_back(i); + } } - } - ticket.m_loadAll = false; + else + { + for (uint32_t i = 0; i < entitiesToSpawnSize; ++i) + { + // If this entity has previously been spawned, give it a new id in the reference map + RefreshEntityIdMapping( + entitiesToSpawn[i].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned); - // Let other systems know about newly spawned entities for any pre-processing before adding to the scene/game context. - if (request.m_preInsertionCallback) - { - request.m_preInsertionCallback(request.m_ticketId, SpawnableEntityContainerView( - ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end())); - } + if (aliasIt == aliasEnd || aliasIt->m_sourceIndex != i) + { + AZ::Entity* clone = + CloneSingleEntity(*entitiesToSpawn[i], ticket.m_entityIdReferenceMap, *request.m_serializeContext); + spawnedEntities.emplace_back(clone); + spawnedEntityIndices.push_back(i); + } + else + { + // The list of entities has already been sorted and optimized (See SpawnableEntitiesAliasList:Optimize) so can + // be safely executed in order without risking an invalid state. + AZ::Entity* previousEntity = nullptr; + do + { + AZ::Entity* clone = CloneSingleAliasedEntity( + *entitiesToSpawn[i], *aliasIt, ticket.m_entityIdReferenceMap, previousEntity, + *request.m_serializeContext); + // Not all alias operations create a new instance. It's also possible for an empty entity to be left behind, + // in which case it's also filtered out as the entity component framework doesn't handle these gracefully. + if (clone) + { + if (!clone->GetComponents().empty()) + { + previousEntity = clone; + spawnedEntities.emplace_back(clone); + spawnedEntityIndices.push_back(i); + } + else + { + delete clone; + } + } + ++aliasIt; + } while (aliasIt != aliasEnd && aliasIt->m_sourceIndex == i); + } + } + } - // Add to the game context, now the entities are active - for (auto it = ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount; it != ticket.m_spawnedEntities.end(); ++it) - { + // There were no initial entities then the ticket now holds exactly all entities. If there were already entities then + // a new set are not added so it no longer holds exactly the number of entities. + ticket.m_loadAll = spawnedEntitiesInitialCount == 0; + + auto newEntitiesBegin = ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount; + auto newEntitiesEnd = ticket.m_spawnedEntities.end(); + // Let other systems know about newly spawned entities for any pre-processing before adding to the scene/game context. + if (request.m_preInsertionCallback) + { + request.m_preInsertionCallback(request.m_ticketId, SpawnableEntityContainerView(newEntitiesBegin, newEntitiesEnd)); + } + + // Add to the game context, now the entities are active + for (auto it = newEntitiesBegin; it != newEntitiesEnd; ++it) + { (*it)->SetSpawnTicketId(request.m_ticketId); - GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, *it); - } + GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, *it); + } - if (request.m_completionCallback) - { - request.m_completionCallback(request.m_ticketId, SpawnableConstEntityContainerView( - ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end())); - } + // Let other systems know about newly spawned entities for any post-processing after adding to the scene/game context. + if (request.m_completionCallback) + { + request.m_completionCallback(request.m_ticketId, SpawnableConstEntityContainerView(newEntitiesBegin, newEntitiesEnd)); + } - ticket.m_currentRequestId++; - return true; - } - else - { - return false; + ticket.m_currentRequestId++; + return CommandResult::Executed; + } } + return CommandResult::Requeue; } - bool SpawnableEntitiesManager::ProcessRequest(DespawnAllEntitiesCommand& request) + auto SpawnableEntitiesManager::ProcessRequest(SpawnEntitiesCommand& request) -> CommandResult + { + Ticket& ticket = *request.m_ticket; + if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId) + { + if (Spawnable::EntityAliasConstVisitor aliases = ticket.m_spawnable->TryGetAliasesConst(); + aliases.HasLock() && aliases.AreAllSpawnablesReady()) + { + 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 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(); + + if (ticket.m_entityIdReferenceMap.empty() || !request.m_referencePreviouslySpawnedEntities) + { + // This map keeps track of ids from template (spawnable) to clone (instance) allowing patch ups of fields referring + // to entityIds outside of a given entity. + // We pre-generate the full set of entity id to new entity id mappings, so that during the clone operation below, + // any entity references that point to a not-yet-cloned entity will still get their ids remapped correctly. + // By default, we only initialize this map once because it needs to persist across multiple SpawnEntities calls, so + // that reference fixups work even when the entity being referenced is spawned in a different SpawnEntities + // (or SpawnAllEntities) call. + // However, the caller can also choose to reset the map by passing in "m_referencePreviouslySpawnedEntities = false". + InitializeEntityIdMappings(entitiesToSpawn, ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned); + } + + spawnedEntities.reserve(spawnedEntities.size() + entitiesToSpawnSize); + spawnedEntityIndices.reserve(spawnedEntityIndices.size() + entitiesToSpawnSize); + + auto aliasBegin = aliases.begin(); + auto aliasEnd = aliases.end(); + if (aliasBegin == aliasEnd) + { + for (uint32_t index : request.m_entityIndices) + { + if (index < entitiesToSpawn.size()) + { + // If this entity has previously been spawned, give it a new id in the reference map + RefreshEntityIdMapping( + entitiesToSpawn[index].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned); + + AZ::Entity* clone = + CloneSingleEntity(*entitiesToSpawn[index], ticket.m_entityIdReferenceMap, *request.m_serializeContext); + AZ_Assert(clone != nullptr, "Failed to clone spawnable entity."); + spawnedEntities.push_back(clone); + spawnedEntityIndices.push_back(index); + } + } + } + else + { + for (uint32_t index : request.m_entityIndices) + { + if (index < entitiesToSpawn.size()) + { + // If this entity has previously been spawned, give it a new id in the reference map + RefreshEntityIdMapping( + entitiesToSpawn[index].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned); + + auto aliasIt = AZStd::lower_bound( + aliasBegin, aliasEnd, index, + [](const Spawnable::EntityAlias& lhs, uint32_t rhs) + { + return lhs.m_sourceIndex < rhs; + }); + + if (aliasIt == aliasEnd) + { + AZ::Entity* clone = + CloneSingleEntity(*entitiesToSpawn[index], ticket.m_entityIdReferenceMap, *request.m_serializeContext); + spawnedEntities.emplace_back(clone); + spawnedEntityIndices.push_back(index); + } + else + { + // The list of entities has already been sorted and optimized (See SpawnableEntitiesAliasList:Optimize) so + // can be safely executed in order without risking an invalid state. + AZ::Entity* previousEntity = nullptr; + do + { + AZ::Entity* clone = CloneSingleAliasedEntity( + *entitiesToSpawn[index], *aliasIt, ticket.m_entityIdReferenceMap, previousEntity, + *request.m_serializeContext); + // Not all alias operations create a new instance. It's also possible for an empty entity to be left + // behind, in which case it's also filtered out as the entity component framework doesn't handle these + // gracefully. + if (clone) + { + if (!clone->GetComponents().empty()) + { + previousEntity = clone; + spawnedEntities.emplace_back(clone); + spawnedEntityIndices.push_back(index); + } + else + { + delete clone; + } + } + ++aliasIt; + } while (aliasIt != aliasEnd && aliasIt->m_sourceIndex == index); + } + } + } + } + ticket.m_loadAll = false; + + // Let other systems know about newly spawned entities for any pre-processing before adding to the scene/game context. + if (request.m_preInsertionCallback) + { + request.m_preInsertionCallback( + request.m_ticketId, + SpawnableEntityContainerView( + ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end())); + } + + // Add to the game context, now the entities are active + for (auto it = ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount; it != ticket.m_spawnedEntities.end(); ++it) + { + (*it)->SetSpawnTicketId(request.m_ticketId); + GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, *it); + } + + if (request.m_completionCallback) + { + request.m_completionCallback( + request.m_ticketId, + SpawnableConstEntityContainerView( + ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end())); + } + + ticket.m_currentRequestId++; + return CommandResult::Executed; + } + } + return CommandResult::Requeue; + } + + auto SpawnableEntitiesManager::ProcessRequest(DespawnAllEntitiesCommand& request) -> CommandResult { Ticket& ticket = *request.m_ticket; if (request.m_requestId == ticket.m_currentRequestId) @@ -495,15 +707,15 @@ namespace AzFramework } ticket.m_currentRequestId++; - return true; + return CommandResult::Executed; } else { - return false; + return CommandResult::Requeue; } } - bool SpawnableEntitiesManager::ProcessRequest(DespawnEntityCommand& request) + auto SpawnableEntitiesManager::ProcessRequest(DespawnEntityCommand& request) -> CommandResult { Ticket& ticket = *request.m_ticket; if (request.m_requestId == ticket.m_currentRequestId) @@ -529,15 +741,15 @@ namespace AzFramework } ticket.m_currentRequestId++; - return true; + return CommandResult::Executed; } else { - return false; + return CommandResult::Requeue; } } - bool SpawnableEntitiesManager::ProcessRequest(ReloadSpawnableCommand& request) + auto SpawnableEntitiesManager::ProcessRequest(ReloadSpawnableCommand& request) -> CommandResult { Ticket& ticket = *request.m_ticket; AZ_Assert(ticket.m_spawnable.GetId() == request.m_spawnable.GetId(), @@ -574,7 +786,7 @@ namespace AzFramework ticket.m_spawnedEntityIndices.clear(); size_t entitiesToSpawnSize = entities.size(); - for (size_t i = 0; i < entitiesToSpawnSize; ++i) + for (uint32_t i = 0; i < entitiesToSpawnSize; ++i) { // If this entity has previously been spawned, give it a new id in the reference map RefreshEntityIdMapping(entities[i].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned); @@ -590,7 +802,7 @@ namespace AzFramework { size_t entitiesSize = entities.size(); - for (size_t index : ticket.m_spawnedEntityIndices) + for (uint32_t index : ticket.m_spawnedEntityIndices) { // 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 @@ -616,15 +828,47 @@ namespace AzFramework ticket.m_currentRequestId++; - return true; + return CommandResult::Executed; } else { - return false; + return CommandResult::Requeue; } } - bool SpawnableEntitiesManager::ProcessRequest(ListEntitiesCommand& request) + auto SpawnableEntitiesManager::ProcessRequest(UpdateEntityAliasTypesCommand& request) -> CommandResult + { + Ticket& ticket = *request.m_ticket; + if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId) + { + if (Spawnable::EntityAliasVisitor aliases = ticket.m_spawnable->TryGetAliases(); aliases.HasLock()) + { + for (EntityAliasTypeChange& replacement : request.m_entityAliases) + { + aliases.UpdateAliasType(replacement.m_aliasIndex, replacement.m_newAliasType); + } + aliases.Optimize(); + + if (request.m_completionCallback) + { + request.m_completionCallback(request.m_ticketId); + } + + ticket.m_currentRequestId++; + return CommandResult::Executed; + } + else + { + AZ_Assert( + ticket.m_spawnable->IsPermanentlyLocked(), + "An request to UpdateEntityAliasTypes on the Spawnables Entities Manager was processed on a spawnable that's permanently " + "locked."); + } + } + return CommandResult::Requeue; + } + + auto SpawnableEntitiesManager::ProcessRequest(ListEntitiesCommand& request) -> CommandResult { Ticket& ticket = *request.m_ticket; if (request.m_requestId == ticket.m_currentRequestId) @@ -632,15 +876,15 @@ namespace AzFramework request.m_listCallback(request.m_ticketId, SpawnableConstEntityContainerView( ticket.m_spawnedEntities.begin(), ticket.m_spawnedEntities.end())); ticket.m_currentRequestId++; - return true; + return CommandResult::Executed; } else { - return false; + return CommandResult::Requeue; } } - bool SpawnableEntitiesManager::ProcessRequest(ListIndicesEntitiesCommand& request) + auto SpawnableEntitiesManager::ProcessRequest(ListIndicesEntitiesCommand& request) -> CommandResult { Ticket& ticket = *request.m_ticket; if (request.m_requestId == ticket.m_currentRequestId) @@ -651,15 +895,15 @@ namespace AzFramework request.m_listCallback(request.m_ticketId, SpawnableConstIndexEntityContainerView( ticket.m_spawnedEntities.begin(), ticket.m_spawnedEntityIndices.begin(), ticket.m_spawnedEntities.size())); ticket.m_currentRequestId++; - return true; + return CommandResult::Executed; } else { - return false; + return CommandResult::Requeue; } } - bool SpawnableEntitiesManager::ProcessRequest(ClaimEntitiesCommand& request) + auto SpawnableEntitiesManager::ProcessRequest(ClaimEntitiesCommand& request) -> CommandResult { Ticket& ticket = *request.m_ticket; if (request.m_requestId == ticket.m_currentRequestId) @@ -671,15 +915,15 @@ namespace AzFramework ticket.m_spawnedEntityIndices.clear(); ticket.m_currentRequestId++; - return true; + return CommandResult::Executed; } else { - return false; + return CommandResult::Requeue; } } - bool SpawnableEntitiesManager::ProcessRequest(BarrierCommand& request) + auto SpawnableEntitiesManager::ProcessRequest(BarrierCommand& request) -> CommandResult { Ticket& ticket = *request.m_ticket; if (request.m_requestId == ticket.m_currentRequestId) @@ -690,15 +934,39 @@ namespace AzFramework } ticket.m_currentRequestId++; - return true; + return CommandResult::Executed; } else { - return false; + return CommandResult::Requeue; } } - bool SpawnableEntitiesManager::ProcessRequest(DestroyTicketCommand& request) + auto SpawnableEntitiesManager::ProcessRequest(LoadBarrierCommand& request) -> CommandResult + { + Ticket& ticket = *request.m_ticket; + if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId) + { + if (request.m_checkAliasSpawnables) + { + if (Spawnable::EntityAliasConstVisitor visitor = ticket.m_spawnable->TryGetAliasesConst(); + !visitor.HasLock() || !visitor.AreAllSpawnablesReady()) + { + return CommandResult::Requeue; + } + } + + request.m_completionCallback(request.m_ticketId); + ticket.m_currentRequestId++; + return CommandResult::Executed; + } + else + { + return CommandResult::Requeue; + } + } + + auto SpawnableEntitiesManager::ProcessRequest(DestroyTicketCommand& request) -> CommandResult { if (request.m_requestId == request.m_ticket->m_currentRequestId) { @@ -714,11 +982,11 @@ namespace AzFramework } delete request.m_ticket; - return true; + return CommandResult::Executed; } else { - return false; + return CommandResult::Requeue; } } } // namespace AzFramework diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h index c3de5be003..09e9b1acfc 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h @@ -55,13 +55,18 @@ namespace AzFramework void SpawnAllEntities(EntitySpawnTicket& ticket, SpawnAllEntitiesOptionalArgs optionalArgs = {}) override; void SpawnEntities( - EntitySpawnTicket& ticket, AZStd::vector entityIndices, SpawnEntitiesOptionalArgs optionalArgs = {}) override; + EntitySpawnTicket& ticket, AZStd::vector entityIndices, SpawnEntitiesOptionalArgs optionalArgs = {}) override; void DespawnAllEntities(EntitySpawnTicket& ticket, DespawnAllEntitiesOptionalArgs optionalArgs = {}) override; void DespawnEntity(AZ::EntityId entityId, EntitySpawnTicket& ticket, DespawnEntityOptionalArgs optionalArgs = {}) override; void RetrieveEntitySpawnTicket(EntitySpawnTicket::Id entitySpawnTicketId, RetrieveEntitySpawnTicketCallback callback) override; void ReloadSpawnable( EntitySpawnTicket& ticket, AZ::Data::Asset spawnable, ReloadSpawnableOptionalArgs optionalArgs = {}) override; + void UpdateEntityAliasTypes( + EntitySpawnTicket& ticket, + AZStd::vector updatedAliases, + UpdateEntityAliasTypesOptionalArgs optionalArgs = {}) override; + void ListEntities( EntitySpawnTicket& ticket, ListEntitiesCallback listCallback, ListEntitiesOptionalArgs optionalArgs = {}) override; void ListIndicesAndEntities( @@ -70,6 +75,8 @@ namespace AzFramework EntitySpawnTicket& ticket, ClaimEntitiesCallback listCallback, ClaimEntitiesOptionalArgs optionalArgs = {}) override; void Barrier(EntitySpawnTicket& spawnInfo, BarrierCallback completionCallback, BarrierOptionalArgs optionalArgs = {}) override; + void LoadBarrier( + EntitySpawnTicket& spawnInfo, BarrierCallback completionCallback, LoadBarrierOptionalArgs optionalArgs = {}) override; // // The following function is thread safe but intended to be run from the main thread. @@ -78,7 +85,13 @@ namespace AzFramework CommandQueueStatus ProcessQueue(CommandQueuePriority priority); protected: - struct Ticket + enum class CommandResult : bool + { + Executed, + Requeue + }; + + struct Ticket final { AZ_CLASS_ALLOCATOR(Ticket, AZ::ThreadPoolAllocator, 0); static constexpr uint32_t Processing = AZStd::numeric_limits::max(); @@ -100,14 +113,14 @@ namespace AzFramework AZStd::unordered_set m_previouslySpawned; AZStd::vector m_spawnedEntities; - AZStd::vector m_spawnedEntityIndices; + AZStd::vector m_spawnedEntityIndices; AZ::Data::Asset m_spawnable; uint32_t m_nextRequestId{ 0 }; //!< Next id for this ticket. uint32_t m_currentRequestId { 0 }; //!< The id for the command that should be executed. bool m_loadAll{ true }; }; - struct SpawnAllEntitiesCommand + struct SpawnAllEntitiesCommand final { EntitySpawnCallback m_completionCallback; EntityPreInsertionCallback m_preInsertionCallback; @@ -116,9 +129,9 @@ namespace AzFramework EntitySpawnTicket::Id m_ticketId; uint32_t m_requestId; }; - struct SpawnEntitiesCommand + struct SpawnEntitiesCommand final { - AZStd::vector m_entityIndices; + AZStd::vector m_entityIndices; EntitySpawnCallback m_completionCallback; EntityPreInsertionCallback m_preInsertionCallback; AZ::SerializeContext* m_serializeContext; @@ -127,7 +140,7 @@ namespace AzFramework uint32_t m_requestId; bool m_referencePreviouslySpawnedEntities; }; - struct DespawnAllEntitiesCommand + struct DespawnAllEntitiesCommand final { EntityDespawnCallback m_completionCallback; Ticket* m_ticket; @@ -142,7 +155,7 @@ namespace AzFramework EntitySpawnTicket::Id m_ticketId; uint32_t m_requestId; }; - struct ReloadSpawnableCommand + struct ReloadSpawnableCommand final { AZ::Data::Asset m_spawnable; ReloadSpawnableCallback m_completionCallback; @@ -151,35 +164,51 @@ namespace AzFramework EntitySpawnTicket::Id m_ticketId; uint32_t m_requestId; }; - struct ListEntitiesCommand + struct UpdateEntityAliasTypesCommand final + { + AZStd::vector m_entityAliases; + UpdateEntityAliasTypesCallback m_completionCallback; + Ticket* m_ticket; + EntitySpawnTicket::Id m_ticketId; + uint32_t m_requestId; + }; + struct ListEntitiesCommand final { ListEntitiesCallback m_listCallback; Ticket* m_ticket; EntitySpawnTicket::Id m_ticketId; uint32_t m_requestId; }; - struct ListIndicesEntitiesCommand + struct ListIndicesEntitiesCommand final { ListIndicesEntitiesCallback m_listCallback; Ticket* m_ticket; EntitySpawnTicket::Id m_ticketId; uint32_t m_requestId; }; - struct ClaimEntitiesCommand + struct ClaimEntitiesCommand final { ClaimEntitiesCallback m_listCallback; Ticket* m_ticket; EntitySpawnTicket::Id m_ticketId; uint32_t m_requestId; }; - struct BarrierCommand + struct BarrierCommand final { BarrierCallback m_completionCallback; Ticket* m_ticket; EntitySpawnTicket::Id m_ticketId; uint32_t m_requestId; }; - struct DestroyTicketCommand + struct LoadBarrierCommand final + { + BarrierCallback m_completionCallback; + Ticket* m_ticket; + EntitySpawnTicket::Id m_ticketId; + uint32_t m_requestId; + bool m_checkAliasSpawnables; + }; + struct DestroyTicketCommand final { Ticket* m_ticket; uint32_t m_requestId; @@ -191,10 +220,12 @@ namespace AzFramework DespawnAllEntitiesCommand, DespawnEntityCommand, ReloadSpawnableCommand, + UpdateEntityAliasTypesCommand, ListEntitiesCommand, ListIndicesEntitiesCommand, ClaimEntitiesCommand, BarrierCommand, + LoadBarrierCommand, DestroyTicketCommand>; struct Queue @@ -213,17 +244,30 @@ namespace AzFramework AZ::Entity* CloneSingleEntity( const AZ::Entity& entityTemplate, EntityIdMap& templateToCloneMap, AZ::SerializeContext& serializeContext); + AZ::Entity* CloneSingleAliasedEntity( + const AZ::Entity& entityTemplate, + const Spawnable::EntityAlias& alias, + EntityIdMap& templateToCloneMap, + AZ::Entity* previouslySpawnedEntity, + AZ::SerializeContext& serializeContext); + void AppendComponents( + AZ::Entity& target, + const AZ::Entity::ComponentArrayType& componentTemplates, + EntityIdMap& templateToCloneMap, + AZ::SerializeContext& serializeContext); - bool ProcessRequest(SpawnAllEntitiesCommand& request); - bool ProcessRequest(SpawnEntitiesCommand& request); - bool ProcessRequest(DespawnAllEntitiesCommand& request); - bool ProcessRequest(DespawnEntityCommand& request); - bool ProcessRequest(ReloadSpawnableCommand& request); - bool ProcessRequest(ListEntitiesCommand& request); - bool ProcessRequest(ListIndicesEntitiesCommand& request); - bool ProcessRequest(ClaimEntitiesCommand& request); - bool ProcessRequest(BarrierCommand& request); - bool ProcessRequest(DestroyTicketCommand& request); + CommandResult ProcessRequest(SpawnAllEntitiesCommand& request); + CommandResult ProcessRequest(SpawnEntitiesCommand& request); + CommandResult ProcessRequest(DespawnAllEntitiesCommand& request); + CommandResult ProcessRequest(DespawnEntityCommand& request); + CommandResult ProcessRequest(ReloadSpawnableCommand& request); + CommandResult ProcessRequest(UpdateEntityAliasTypesCommand& request); + CommandResult ProcessRequest(ListEntitiesCommand& request); + CommandResult ProcessRequest(ListIndicesEntitiesCommand& request); + CommandResult ProcessRequest(ClaimEntitiesCommand& request); + CommandResult ProcessRequest(BarrierCommand& request); + CommandResult ProcessRequest(LoadBarrierCommand& request); + CommandResult ProcessRequest(DestroyTicketCommand& request); //! Generate a base set of original-to-new entity ID mappings to use during spawning. //! Since Entity references get fixed up on an entity-by-entity basis while spawning, it's important to have the complete From b3cd33990444f428e2e5f7399de6e6372976199c Mon Sep 17 00:00:00 2001 From: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> Date: Mon, 25 Oct 2021 14:55:04 -0700 Subject: [PATCH 04/14] Added support for setting up entity aliases during the prefab to spawnable conversion. Signed-off-by: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> --- .../Prefab/Instance/Instance.cpp | 173 +++++++++++-- .../Prefab/Instance/Instance.h | 39 ++- .../Spawnable/PrefabCatchmentProcessor.cpp | 56 ++-- .../Spawnable/PrefabConversionPipeline.cpp | 1 + .../Spawnable/PrefabProcessorContext.cpp | 136 +++++++++- .../Prefab/Spawnable/PrefabProcessorContext.h | 85 +++++- .../Prefab/Spawnable/ProcesedObjectStore.cpp | 18 +- .../Prefab/Spawnable/ProcesedObjectStore.h | 16 +- .../Prefab/Spawnable/SpawnableUtils.cpp | 242 +++++++++++++++++- .../Prefab/Spawnable/SpawnableUtils.h | 45 ++++ .../PrefabBuilder/PrefabBuilderComponent.cpp | 21 +- 11 files changed, 743 insertions(+), 89 deletions(-) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/Instance.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/Instance.cpp index b5db46d0db..490a151925 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/Instance.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/Instance.cpp @@ -129,7 +129,7 @@ namespace AzToolsFramework void Instance::SetLinkId(LinkId linkId) { - m_linkId = AZStd::move(linkId); + m_linkId = linkId; } LinkId Instance::GetLinkId() const @@ -154,23 +154,26 @@ namespace AzToolsFramework bool Instance::AddEntity(AZ::Entity& entity) { - EntityAlias newEntityAlias = GenerateEntityAlias(); - return AddEntity(entity, newEntityAlias); + return AddEntity(entity, GenerateEntityAlias()); + } + + bool Instance::AddEntity(AZStd::unique_ptr&& entity) + { + return AddEntity(AZStd::move(entity), GenerateEntityAlias()); } bool Instance::AddEntity(AZ::Entity& entity, EntityAlias entityAlias) { - if (!RegisterEntity(entity.GetId(), entityAlias)) - { - return false; - } + return + RegisterEntity(entity.GetId(), entityAlias) && + m_entities.emplace(AZStd::move(entityAlias), &entity).second; + } - if (!m_entities.emplace(AZStd::make_pair(entityAlias, &entity)).second) - { - return false; - } - - return true; + bool Instance::AddEntity(AZStd::unique_ptr&& entity, EntityAlias entityAlias) + { + return + RegisterEntity(entity->GetId(), entityAlias) && + m_entities.emplace(AZStd::move(entityAlias), AZStd::move(entity)).second; } AZStd::unique_ptr Instance::DetachEntity(const AZ::EntityId& entityId) @@ -228,6 +231,23 @@ namespace AzToolsFramework m_entities.clear(); } + AZStd::unique_ptr Instance::ReplaceEntity(AZStd::unique_ptr&& entity, EntityAliasView alias) + { + AZStd::unique_ptr result; + auto it = m_entities.find(alias); + if (it != m_entities.end()) + { + // Swap entity ids as these need to remain stable + AZ::EntityId originalId = it->second->GetId(); + it->second->SetId(entity->GetId()); + entity->SetId(originalId); + + result = AZStd::move(it->second); + it->second = AZStd::move(entity); + } + return result; + } + void Instance::RemoveNestedEntities( const AZStd::function&)>& filter) { @@ -377,7 +397,12 @@ namespace AzToolsFramework return entityAliases; } - void Instance::GetNestedEntityIds(const AZStd::function& callback) + size_t Instance::GetEntityAliasCount() const + { + return m_entities.size(); + } + + void Instance::GetNestedEntityIds(const AZStd::function& callback) const { GetEntityIds(callback); @@ -387,7 +412,7 @@ namespace AzToolsFramework } } - void Instance::GetEntityIds(const AZStd::function& callback) + void Instance::GetEntityIds(const AZStd::function& callback) const { for (auto&&[entityAlias, entityId] : m_templateToInstanceEntityIdMap) { @@ -398,6 +423,17 @@ namespace AzToolsFramework } } + void Instance::GetEntityIdToAlias(const AZStd::function& callback) const + { + for (auto&& [entityAlias, entityId] : m_templateToInstanceEntityIdMap) + { + if (!callback(entityId, entityAlias)) + { + break; + } + } + } + bool Instance::GetEntities_Impl(const AZStd::function&)>& callback) { for (auto& [entityAlias, entity] : m_entities) @@ -514,24 +550,81 @@ namespace AzToolsFramework } } - EntityAliasOptionalReference Instance::GetEntityAlias(const AZ::EntityId& id) + EntityAliasOptionalReference Instance::GetEntityAlias(AZ::EntityId id) { - if (m_instanceToTemplateEntityIdMap.count(id)) - { - return m_instanceToTemplateEntityIdMap[id]; - } - - return AZStd::nullopt; + auto it = m_instanceToTemplateEntityIdMap.find(id); + return it != m_instanceToTemplateEntityIdMap.end() ? EntityAliasOptionalReference(it->second) + : EntityAliasOptionalReference(AZStd::nullopt); } - AZ::EntityId Instance::GetEntityId(const EntityAlias& alias) + EntityAliasView Instance::GetEntityAlias(AZ::EntityId id) const { - if (m_templateToInstanceEntityIdMap.count(alias)) + auto it = m_instanceToTemplateEntityIdMap.find(id); + return it != m_instanceToTemplateEntityIdMap.end() ? EntityAliasView(it->second) : EntityAliasView(); + } + + AZStd::pair Instance::FindInstanceAndAlias(AZ::EntityId entity) + { + auto it = m_instanceToTemplateEntityIdMap.find(entity); + if (it != m_instanceToTemplateEntityIdMap.end()) { - return m_templateToInstanceEntityIdMap[alias]; + return AZStd::pair(this, it->second); } - - return AZ::EntityId(); + else + { + for (auto&& [_, instance] : m_nestedInstances) + { + AZStd::pair next = instance->FindInstanceAndAlias(entity); + if (next.first != nullptr) + { + return next; + } + } + } + return AZStd::pair(nullptr, ""); + } + + AZStd::pair Instance::FindInstanceAndAlias(AZ::EntityId entity) const + { + return const_cast(this)->FindInstanceAndAlias(entity); + } + + EntityOptionalReference Instance::GetEntity(const EntityAlias& alias) + { + auto it = m_entities.find(alias); + return it != m_entities.end() ? EntityOptionalReference(*it->second) : EntityOptionalReference(AZStd::nullopt); + } + + EntityOptionalConstReference Instance::GetEntity(const EntityAlias& alias) const + { + auto it = m_entities.find(alias); + return it != m_entities.end() ? EntityOptionalConstReference(*it->second) : EntityOptionalConstReference(AZStd::nullopt); + } + + AZ::EntityId Instance::GetEntityId(const EntityAlias& alias) const + { + auto it = m_templateToInstanceEntityIdMap.find(alias); + return it != m_templateToInstanceEntityIdMap.end() ? it->second : AZ::EntityId(); + } + + AZ::EntityId Instance::GetEntityIdFromAliasPath(AliasPathView relativeAliasPath) const + { + const Instance* instance = this; + AliasPathView path = relativeAliasPath.ParentPath(); + for (auto it : path) + { + InstanceOptionalConstReference child = instance->FindNestedInstance(it.Native()); + if (child.has_value()) + { + instance = &(child->get()); + } + else + { + return AZ::EntityId(); + } + } + + return instance->GetEntityId(relativeAliasPath.Filename().Native()); } AZStd::vector Instance::GetNestedInstanceAliases(TemplateId templateId) const @@ -572,6 +665,32 @@ namespace AzToolsFramework return aliasPathResult; } + AliasPath Instance::GetAliasPathRelativeToInstance(const AZ::EntityId& entity) const + { + AliasPath result = AliasPath(s_aliasPathSeparator); + auto&& [instance, alias] = FindInstanceAndAlias(entity); + if (instance) + { + AZStd::vector instanceChain; + + while (instance && instance != this) + { + instanceChain.push_back(instance); + instance = instance->m_parent; + } + + for (auto it = instanceChain.rbegin(); it != instanceChain.rend(); ++it) + { + result.Append((*it)->m_alias); + } + return result.Append(alias); + } + else + { + return result; + } + } + EntityAlias Instance::GenerateEntityAlias() { return AZStd::string::format("Entity_%s", AZ::Entity::MakeId().ToString().c_str()); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/Instance.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/Instance.h index 50a39268fe..25971093cd 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/Instance.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/Instance.h @@ -38,6 +38,7 @@ namespace AzToolsFramework using AliasPath = AZ::IO::Path; using AliasPathView = AZ::IO::PathView; using EntityAlias = AZStd::string; + using EntityAliasView = AZStd::string_view; using InstanceAlias = AZStd::string; class Instance; @@ -83,9 +84,17 @@ namespace AzToolsFramework void SetContainerEntityName(AZStd::string_view containerName); bool AddEntity(AZ::Entity& entity); + bool AddEntity(AZStd::unique_ptr&& entity); bool AddEntity(AZ::Entity& entity, EntityAlias entityAlias); + bool AddEntity(AZStd::unique_ptr&& entity, EntityAlias entityAlias); AZStd::unique_ptr DetachEntity(const AZ::EntityId& entityId); void DetachEntities(const AZStd::function)>& callback); + /** + * Replaces the entity stored under the provided alias with a new one. + * + * @return The original entity or a nullptr if not found. + */ + AZStd::unique_ptr ReplaceEntity(AZStd::unique_ptr&& entity, EntityAliasView alias); /** * Detaches all entities in the instance hierarchy. @@ -109,13 +118,15 @@ namespace AzToolsFramework * @return The list of EntityAliases */ AZStd::vector GetEntityAliases(); + size_t GetEntityAliasCount() const; /** * Gets the ids for the entities in the Instance DOM. Can recursively trace all nested instances. */ - void GetNestedEntityIds(const AZStd::function& callback); + void GetNestedEntityIds(const AZStd::function& callback) const; - void GetEntityIds(const AZStd::function& callback); + void GetEntityIds(const AZStd::function& callback) const; + void GetEntityIdToAlias(const AZStd::function& callback) const; /** * Gets the entities in the Instance DOM. Can recursively trace all nested instances. @@ -131,14 +142,33 @@ namespace AzToolsFramework * * @return entityAlias via optional */ - AZStd::optional> GetEntityAlias(const AZ::EntityId& id); + EntityAliasOptionalReference GetEntityAlias(AZ::EntityId id); + EntityAliasView GetEntityAlias(AZ::EntityId id) const; + /** + * Searches for the entity in this instance and its nested instances. + * + * @return The instance that owns the entity and the alias under which the entity is known. + * If the entity isn't found then the instance will be null and the alias empty. + */ + AZStd::pair FindInstanceAndAlias(AZ::EntityId entity); + AZStd::pair FindInstanceAndAlias(AZ::EntityId entity) const; + + EntityOptionalReference GetEntity(const EntityAlias& alias); + EntityOptionalConstReference GetEntity(const EntityAlias& alias) const; /** * Gets the id for a given EnitityAlias in the Instance DOM. * * @return entityId, invalid ID if not found */ - AZ::EntityId GetEntityId(const EntityAlias& alias); + AZ::EntityId GetEntityId(const EntityAlias& alias) const; + + /** + * Retrieves the entity id from an alias path that's relative to this instance. + * + * @return entityId, invalid ID if not found + */ + AZ::EntityId GetEntityIdFromAliasPath(AliasPathView relativeAliasPath) const; /** @@ -180,6 +210,7 @@ namespace AzToolsFramework static EntityAlias GenerateEntityAlias(); AliasPath GetAbsoluteInstanceAliasPath() const; + AliasPath GetAliasPathRelativeToInstance(const AZ::EntityId& entity) const; static InstanceAlias GenerateInstanceAlias(); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabCatchmentProcessor.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabCatchmentProcessor.cpp index 7b7107ae3e..7a54950c47 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabCatchmentProcessor.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabCatchmentProcessor.cpp @@ -13,9 +13,12 @@ #include #include #include +#include +#include #include #include + namespace AzToolsFramework::Prefab::PrefabConversionUtils { void PrefabCatchmentProcessor::Process(PrefabProcessorContext& context) @@ -37,7 +40,7 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils ->Value("Text", SerializationFormats::Text); serializeContext->Class() - ->Version(2) + ->Version(3) ->Field("SerializationFormat", &PrefabCatchmentProcessor::m_serializationFormat); } } @@ -45,6 +48,8 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils void PrefabCatchmentProcessor::ProcessPrefab(PrefabProcessorContext& context, AZStd::string_view prefabName, PrefabDom& prefab, AZ::DataStream::StreamType serializationFormat) { + using namespace AzToolsFramework::Prefab::SpawnableUtils; + AZStd::string uniqueName = prefabName; uniqueName += AzFramework::Spawnable::DotFileExtension; @@ -59,33 +64,38 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils AZStd::move(uniqueName), context.GetSourceUuid(), AZStd::move(serializer)); AZ_Assert(spawnable, "Failed to create a new spawnable."); - bool result = SpawnableUtils::CreateSpawnable(*spawnable, prefab, object.GetReferencedAssets()); - if (result) + Instance instance; + if (Prefab::PrefabDomUtils::LoadInstanceFromPrefabDom( + instance, prefab, object.GetReferencedAssets(), + Prefab::PrefabDomUtils::LoadFlags::AssignRandomEntityId)) // Always assign random entity ids because the spawnable is + // going to be used to create clones of the entities. { + // Resolve entity aliases that store PrefabDOM information to use the spawnable instead. This is done before the entities are + // moved from the instance as they'd otherwise can't be found. + context.ResolveSpawnableEntityAliases(prefabName, *spawnable, instance); + AzFramework::Spawnable::EntityList& entities = spawnable->GetEntities(); - for (auto it = entities.begin(); it != entities.end(); ) - { - if (*it) + instance.DetachAllEntitiesInHierarchy( + [&entities, &context](AZStd::unique_ptr entity) { - (*it)->InvalidateDependencies(); - AZ::Entity::DependencySortOutcome evaluation = (*it)->EvaluateDependenciesGetDetails(); - if (evaluation.IsSuccess()) + if (entity) { - ++it; + entity->InvalidateDependencies(); + AZ::Entity::DependencySortOutcome evaluation = entity->EvaluateDependenciesGetDetails(); + if (evaluation.IsSuccess()) + { + entities.emplace_back(AZStd::move(entity)); + } + else + { + AZ_Error( + "Prefabs", false, "Entity '%s' %s cannot be activated for the following reason: %s", + entity->GetName().c_str(), entity->GetId().ToString().c_str(), evaluation.GetError().m_message.c_str()); + context.ErrorEncountered(); + } } - else - { - AZ_Error( - "Prefabs", false, "Entity '%s' %s cannot be activated for the following reason: %s", (*it)->GetName().c_str(), - (*it)->GetId().ToString().c_str(), evaluation.GetError().m_message.c_str()); - it = entities.erase(it); - } - } - else - { - it = entities.erase(it); - } - } + }); + SpawnableUtils::SortEntitiesByTransformHierarchy(*spawnable); context.GetProcessedObjects().push_back(AZStd::move(object)); } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabConversionPipeline.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabConversionPipeline.cpp index ac3ada7557..c40f0d2b2e 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabConversionPipeline.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabConversionPipeline.cpp @@ -54,6 +54,7 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils { processor->Process(context); } + context.ResolveLinks(); } size_t PrefabConversionPipeline::CalculateProcessorFingerprint(AZ::SerializeContext* context) { diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.cpp index 0a7fb3a224..e12ddb616f 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.cpp @@ -6,12 +6,26 @@ * */ +#include #include - +#include #include +#include namespace AzToolsFramework::Prefab::PrefabConversionUtils { + EntityAliasSpawnableLink::EntityAliasSpawnableLink(AzFramework::Spawnable& spawnable, AZ::EntityId index) + : m_spawnable(spawnable) + , m_index(index) + { + } + + EntityAliasPrefabLink::EntityAliasPrefabLink(AZStd::string prefabName, AzToolsFramework::Prefab::AliasPath alias) + : m_prefabName(AZStd::move(prefabName)) + , m_alias(AZStd::move(alias)) + { + } + PrefabProcessorContext::PrefabProcessorContext(const AZ::Uuid& sourceUuid) : m_sourceUuid(sourceUuid) {} @@ -45,7 +59,8 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils return !m_prefabs.empty(); } - bool PrefabProcessorContext::RegisterSpawnableProductAssetDependency(AZStd::string prefabName, AZStd::string dependentPrefabName) + bool PrefabProcessorContext::RegisterSpawnableProductAssetDependency( + AZStd::string prefabName, AZStd::string dependentPrefabName, EntityAliasSpawnableLoadBehavior loadBehavior) { using ConversionUtils = PrefabConversionUtils::ProcessedObjectStore; @@ -55,10 +70,11 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils dependentPrefabName += AzFramework::Spawnable::DotFileExtension; uint32_t spawnablePrefabSubId = ConversionUtils::BuildSubId(AZStd::move(dependentPrefabName)); - return RegisterSpawnableProductAssetDependency(spawnableSubId, spawnablePrefabSubId); + return RegisterSpawnableProductAssetDependency(spawnableSubId, spawnablePrefabSubId, loadBehavior); } - bool PrefabProcessorContext::RegisterSpawnableProductAssetDependency(AZStd::string prefabName, const AZ::Data::AssetId& dependentAssetId) + bool PrefabProcessorContext::RegisterSpawnableProductAssetDependency( + AZStd::string prefabName, const AZ::Data::AssetId& dependentAssetId, EntityAliasSpawnableLoadBehavior loadBehavior) { using ConversionUtils = PrefabConversionUtils::ProcessedObjectStore; @@ -67,20 +83,78 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils AZ::Data::AssetId spawnableAssetId(GetSourceUuid(), spawnableSubId); - return RegisterProductAssetDependency(spawnableAssetId, dependentAssetId); + return RegisterProductAssetDependency(spawnableAssetId, dependentAssetId, ToAssetLoadBehavior(loadBehavior)); } - bool PrefabProcessorContext::RegisterSpawnableProductAssetDependency(uint32_t spawnableAssetSubId, uint32_t dependentSpawnableAssetSubId) + bool PrefabProcessorContext::RegisterSpawnableProductAssetDependency( + uint32_t spawnableAssetSubId, uint32_t dependentSpawnableAssetSubId, EntityAliasSpawnableLoadBehavior loadBehavior) { AZ::Data::AssetId spawnableAssetId(GetSourceUuid(), spawnableAssetSubId); AZ::Data::AssetId dependentSpawnableAssetId(GetSourceUuid(), dependentSpawnableAssetSubId); - return RegisterProductAssetDependency(spawnableAssetId, dependentSpawnableAssetId); + return RegisterProductAssetDependency(spawnableAssetId, dependentSpawnableAssetId, ToAssetLoadBehavior(loadBehavior)); } bool PrefabProcessorContext::RegisterProductAssetDependency(const AZ::Data::AssetId& assetId, const AZ::Data::AssetId& dependentAssetId) { - return m_registeredProductAssetDependencies[assetId].emplace(dependentAssetId).second; + return RegisterProductAssetDependency(assetId, dependentAssetId, AZ::Data::AssetLoadBehavior::NoLoad); + } + + bool PrefabProcessorContext::RegisterProductAssetDependency( + const AZ::Data::AssetId& assetId, const AZ::Data::AssetId& dependentAssetId, AZ::Data::AssetLoadBehavior loadBehavior) + { + auto dependencies = m_registeredProductAssetDependencies.equal_range(assetId); + if (dependencies.first != dependencies.second) + { + for (auto it = dependencies.first; it != dependencies.second; ++it) + { + if (it->second.m_assetId == dependentAssetId) + { + if (it->second.m_loadBehavior < loadBehavior) + { + it->second.m_loadBehavior = loadBehavior; + } + return true; + } + } + } + + return m_registeredProductAssetDependencies.emplace(assetId, AssetDependencyInfo{ dependentAssetId, loadBehavior }).second; + } + + void PrefabProcessorContext::RegisterSpawnableEntityAlias(EntityAliasStore link) + { + m_entityAliases.push_back(AZStd::move(link)); + } + + void PrefabProcessorContext::ResolveSpawnableEntityAliases( + AZStd::string_view prefabName, AzFramework::Spawnable& spawnable, const AzToolsFramework::Prefab::Instance& instance) + { + using namespace AzToolsFramework::Prefab; + + for (EntityAliasStore& entityAlias : m_entityAliases) + { + auto sourcePrefab = AZStd::get_if(&entityAlias.m_source); + if (sourcePrefab != nullptr && sourcePrefab->m_prefabName == prefabName) + { + AZ::EntityId id = instance.GetEntityIdFromAliasPath(sourcePrefab->m_alias); + AZ_Assert( + id.IsValid(), + "Entity '%s' was not found in Prefab Instance created from '%s' even though it was previously found.", + sourcePrefab->m_alias.c_str(), sourcePrefab->m_prefabName.c_str()); + entityAlias.m_source.emplace(spawnable, id); + } + + auto targetPrefab = AZStd::get_if(&entityAlias.m_target); + if (targetPrefab != nullptr && targetPrefab->m_prefabName == prefabName) + { + AZ::EntityId id = instance.GetEntityIdFromAliasPath(targetPrefab->m_alias); + AZ_Assert( + id.IsValid(), "Entity '%s' was not found in Prefab Instance created from '%s' even though it was previously found.", + targetPrefab->m_alias.c_str(), targetPrefab->m_prefabName.c_str()); + entityAlias.m_target.emplace(spawnable, id); + } + } } PrefabProcessorContext::ProcessedObjectStoreContainer& PrefabProcessorContext::GetProcessedObjects() @@ -118,6 +192,46 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils return m_sourceUuid; } + void PrefabProcessorContext::ResolveLinks() + { + // Store the aliases visitor here when first encountered to avoid the visitor sorting the aliases for every addition. + // Once this map goes out of scope the visitors will be destroyed and in turn sort their aliases. + AZStd::unordered_map aliasVisitors; + + for (EntityAliasStore& alias : m_entityAliases) + { + auto source = AZStd::get_if(&alias.m_source); + AZ_Assert(source, "Entity alias found that has a source that's not yet resolved to a spawnable"); + auto target = AZStd::get_if(&alias.m_target); + AZ_Assert(target, "Entity alias found that has a target that's not yet resolved to a spawnable"); + + uint32_t sourceIndex = SpawnableUtils::FindEntityIndex(source->m_index, source->m_spawnable); + AZ_Assert( + sourceIndex != SpawnableUtils::InvalidEntityIndex, "Entity %zu not found in source spawnable while resolving to index.", + aznumeric_cast(source->m_index)); + uint32_t targetIndex = SpawnableUtils::FindEntityIndex(target->m_index, target->m_spawnable); + AZ_Assert( + targetIndex != SpawnableUtils::InvalidEntityIndex, "Entity %zu not found in target spawnable while resolving to index.", + aznumeric_cast(target->m_index)); + + AZ::Data::AssetLoadBehavior loadBehavior = ToAssetLoadBehavior(alias.m_loadBehavior); + + auto it = aliasVisitors.find(source->m_spawnable.GetId()); + if (it == aliasVisitors.end()) + { + AzFramework::Spawnable::EntityAliasVisitor visitor = source->m_spawnable.TryGetAliases(); + AZ_Assert(visitor.HasLock(), "Unable to obtain lock for a newly create spawnable."); + it = aliasVisitors.emplace(source->m_spawnable.GetId(), AZStd::move(visitor)).first; + } + it->second.AddAlias( + AZ::Data::Asset(&target->m_spawnable, loadBehavior), alias.m_tag, sourceIndex, targetIndex, + alias.m_aliasType, alias.m_loadBehavior == EntityAliasSpawnableLoadBehavior::QueueLoad); + + // Register the dependency between the two spawnables. + RegisterProductAssetDependency(source->m_spawnable.GetId(), target->m_spawnable.GetId(), loadBehavior); + } + } + bool PrefabProcessorContext::HasCompletedSuccessfully() const { return m_completedSuccessfully; @@ -127,4 +241,10 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils { m_completedSuccessfully = false; } + + AZ::Data::AssetLoadBehavior PrefabProcessorContext::ToAssetLoadBehavior(EntityAliasSpawnableLoadBehavior loadBehavior) const + { + return loadBehavior == EntityAliasSpawnableLoadBehavior::DependentLoad ? AZ::Data::AssetLoadBehavior::PreLoad + : AZ::Data::AssetLoadBehavior::NoLoad; + } } // namespace AzToolsFramework::Prefab::PrefabConversionUtils diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.h index 7c21d446de..7fc01367b2 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.h @@ -9,24 +9,84 @@ #pragma once #include +#include #include #include #include +#include #include #include #include #include +#include +#include #include #include namespace AzToolsFramework::Prefab::PrefabConversionUtils { + enum class EntityAliasType : uint8_t + { + Disabled, //!< No alias is added. + OptionalReplace, //!< At runtime the entity might be replaced. If the alias is disabled the original entity will be spawned. + //!< The original entity will be left in the spawnable and a copy is returned. + Replace, //!< At runtime the entity will be replaced. If the alias is disabled nothing will be spawned not. The original + //!< entity is returned and a blank entity is left. + Additional, //!< At runtime the alias entity will be added as an additional but unrelated entity with a new entity id. + //!< An empty entity will be returned. + Merge //!< At runtime the components in both entities will be merged. An empty entity will be returned. The added + //!< components may no conflict with the entities already in the root entity. + }; + + enum class EntityAliasSpawnableLoadBehavior : uint8_t + { + NoLoad, //!< Don't load the spawnable referenced in the entity alias. Loading will be up to the caller. + QueueLoad, //!< Queue the spawnable referenced in the entity alias for loading. This will be an async load because asset + //!< handlers aren't allowed to start a blocking load as this can lead to deadlocks. + DependentLoad //!< The spawnable referenced in the entity alias is made a dependency of the spawnable that holds the entity + //!< alias. This will cause the spawnable to be automatically loaded along with the owning spawnable. + }; + + struct EntityAliasSpawnableLink + { + EntityAliasSpawnableLink() = default; + EntityAliasSpawnableLink(AzFramework::Spawnable& spawnable, AZ::EntityId index); + + AzFramework::Spawnable& m_spawnable; + AZ::EntityId m_index; + }; + + struct EntityAliasPrefabLink + { + EntityAliasPrefabLink() = default; + EntityAliasPrefabLink(AZStd::string prefabName, AzToolsFramework::Prefab::AliasPath alias); + + AZStd::string m_prefabName; + AzToolsFramework::Prefab::AliasPath m_alias; + }; + + struct EntityAliasStore + { + using LinkStore = AZStd::variant; + + LinkStore m_source; + LinkStore m_target; + uint32_t m_tag; + AzFramework::Spawnable::EntityAliasType m_aliasType; + EntityAliasSpawnableLoadBehavior m_loadBehavior; + }; + + struct AssetDependencyInfo + { + AZ::Data::AssetId m_assetId; + AZ::Data::AssetLoadBehavior m_loadBehavior; + }; + class PrefabProcessorContext { public: using ProcessedObjectStoreContainer = AZStd::vector; - using ProductAssetDependencyContainer = - AZStd::unordered_map>; + using ProductAssetDependencyContainer = AZStd::unordered_multimap; AZ_CLASS_ALLOCATOR(PrefabProcessorContext, AZ::SystemAllocator, 0); AZ_RTTI(PrefabProcessorContext, "{C7D77E3A-C544-486B-B774-7C82C38FE22F}"); @@ -39,11 +99,20 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils virtual void ListPrefabs(const AZStd::function& callback) const; virtual bool HasPrefabs() const; - virtual bool RegisterSpawnableProductAssetDependency(AZStd::string prefabName, AZStd::string dependentPrefabName); - virtual bool RegisterSpawnableProductAssetDependency(AZStd::string prefabName, const AZ::Data::AssetId& dependentAssetId); - virtual bool RegisterSpawnableProductAssetDependency(uint32_t spawnableAssetSubId, uint32_t dependentSpawnableAssetSubId); + virtual bool RegisterSpawnableProductAssetDependency( + AZStd::string prefabName, AZStd::string dependentPrefabName, EntityAliasSpawnableLoadBehavior loadBehavior); + virtual bool RegisterSpawnableProductAssetDependency( + AZStd::string prefabName, const AZ::Data::AssetId& dependentAssetId, EntityAliasSpawnableLoadBehavior loadBehavior); + virtual bool RegisterSpawnableProductAssetDependency( + uint32_t spawnableAssetSubId, uint32_t dependentSpawnableAssetSubId, EntityAliasSpawnableLoadBehavior loadBehavior); virtual bool RegisterProductAssetDependency(const AZ::Data::AssetId& assetId, const AZ::Data::AssetId& dependentAssetId); + virtual bool RegisterProductAssetDependency( + const AZ::Data::AssetId& assetId, const AZ::Data::AssetId& dependentAssetId, AZ::Data::AssetLoadBehavior loadBehavior); + virtual void RegisterSpawnableEntityAlias(EntityAliasStore link); + virtual void ResolveSpawnableEntityAliases( + AZStd::string_view prefabName, AzFramework::Spawnable& spawnable, const AzToolsFramework::Prefab::Instance& instance); + virtual ProcessedObjectStoreContainer& GetProcessedObjects(); virtual const ProcessedObjectStoreContainer& GetProcessedObjects() const; @@ -54,13 +123,19 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils virtual const AZ::PlatformTagSet& GetPlatformTags() const; virtual const AZ::Uuid& GetSourceUuid() const; + virtual void ResolveLinks(); + virtual bool HasCompletedSuccessfully() const; virtual void ErrorEncountered(); protected: using NamedPrefabContainer = AZStd::unordered_map; + using SpawnableEntityAliasStore = AZStd::vector; + + AZ::Data::AssetLoadBehavior ToAssetLoadBehavior(EntityAliasSpawnableLoadBehavior loadBehavior) const; NamedPrefabContainer m_prefabs; + SpawnableEntityAliasStore m_entityAliases; ProcessedObjectStoreContainer m_products; ProductAssetDependencyContainer m_registeredProductAssetDependencies; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/ProcesedObjectStore.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/ProcesedObjectStore.cpp index 9c59b6b559..a6770ee2c7 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/ProcesedObjectStore.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/ProcesedObjectStore.cpp @@ -11,7 +11,16 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils { - ProcessedObjectStore::ProcessedObjectStore(AZStd::string uniqueId, AZStd::unique_ptr asset, SerializerFunction assetSerializer) + void ProcessedObjectStore::AssetSmartPtrDeleter::operator()(AZ::Data::AssetData* asset) + { + if (asset->GetUseCount() == 0) + { + // Only delete the asset if it wasn't turned into a full asset + delete asset; + } + } + + ProcessedObjectStore::ProcessedObjectStore(AZStd::string uniqueId, AssetSmartPtr asset, SerializerFunction assetSerializer) : m_uniqueId(AZStd::move(uniqueId)) , m_assetSerializer(AZStd::move(assetSerializer)) , m_asset(AZStd::move(asset)) @@ -62,7 +71,7 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils return m_referencedAssets; } - AZStd::unique_ptr ProcessedObjectStore::ReleaseAsset() + auto ProcessedObjectStore::ReleaseAsset() -> AssetSmartPtr { return AZStd::move(m_asset); } @@ -72,6 +81,11 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils return AzFramework::SpawnableAssetHandler::BuildSubId(id); } + uint32_t ProcessedObjectStore::GetSubId() const + { + return m_asset->GetId().m_subId; + } + const AZStd::string& ProcessedObjectStore::GetId() const { return m_uniqueId; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/ProcesedObjectStore.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/ProcesedObjectStore.h index 61d529842c..c75e72840e 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/ProcesedObjectStore.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/ProcesedObjectStore.h @@ -26,6 +26,12 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils public: using SerializerFunction = AZStd::function&, const ProcessedObjectStore&)>; + struct AssetSmartPtrDeleter + { + void operator()(AZ::Data::AssetData* asset); + }; + using AssetSmartPtr = AZStd::unique_ptr; + //! Constructs a new instance. //! @param uniqueId A name for the object that's unique within the scope of the Prefab. This name will be used to generate a sub id for the product //! which requires that the name to be stable between runs. @@ -37,24 +43,24 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils bool Serialize(AZStd::vector& output) const; static uint32_t BuildSubId(AZStd::string_view id); + uint32_t GetSubId() const; bool HasAsset() const; const AZ::Data::AssetType& GetAssetType() const; const AZ::Data::AssetData& GetAsset() const; AZ::Data::AssetData& GetAsset(); - AZStd::unique_ptr ReleaseAsset(); + AssetSmartPtr ReleaseAsset(); AZStd::vector>& GetReferencedAssets(); const AZStd::vector>& GetReferencedAssets() const; - const AZStd::string& GetId() const; private: - ProcessedObjectStore(AZStd::string uniqueId, AZStd::unique_ptr asset, SerializerFunction assetSerializer); + ProcessedObjectStore(AZStd::string uniqueId, AssetSmartPtr asset, SerializerFunction assetSerializer); SerializerFunction m_assetSerializer; - AZStd::unique_ptr m_asset; + AssetSmartPtr m_asset; AZStd::vector> m_referencedAssets; AZStd::string m_uniqueId; }; @@ -66,7 +72,7 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils static_assert(AZStd::is_base_of_v, "ProcessedObjectStore can only be created from a class that derives from AZ::Data::AssetData."); AZ::Data::AssetId assetId(sourceId, BuildSubId(uniqueId)); - auto instance = AZStd::make_unique(assetId, AZ::Data::AssetData::AssetStatus::Ready); + auto instance = AssetSmartPtr(aznew T(assetId, AZ::Data::AssetData::AssetStatus::Ready)); ProcessedObjectStore resultLeft(AZStd::move(uniqueId), AZStd::move(instance), AZStd::move(assetSerializer)); T* resultRight = static_cast(&resultLeft.GetAsset()); return AZStd::make_pair(AZStd::move(resultLeft), resultRight); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.cpp index 902f439280..154337e957 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.cpp @@ -8,9 +8,11 @@ #include +#include #include #include #include +#include #include #include #include @@ -20,18 +22,134 @@ namespace AzToolsFramework::Prefab::SpawnableUtils { + namespace Internal + { + AZ::SerializeContext* GetSerializeContext() + { + AZ::SerializeContext* result = nullptr; + AZ::ComponentApplicationBus::BroadcastResult(result, &AZ::ComponentApplicationBus::Events::GetSerializeContext); + AZ_Assert(result, "SpawnbleUtils was unable to locate the Serialize Context."); + return result; + } + + AZ::Entity* FindEntity(AZ::EntityId entity, AzToolsFramework::Prefab::Instance& source) + { + AZ::Entity* result = nullptr; + source.GetEntities( + [&result, entity](AZStd::unique_ptr& instance) + { + if (instance->GetId() != entity) + { + return true; + } + else + { + result = instance.get(); + return false; + } + }); + return result; + } + + AZ::Entity* FindEntity(AZ::EntityId entity, AzFramework::Spawnable& source) + { + uint32_t index = AzToolsFramework::Prefab::SpawnableUtils::FindEntityIndex(entity, source); + return index != InvalidEntityIndex ? source.GetEntities()[index].get() : nullptr; + } + + template + AZStd::unique_ptr CloneEntity(AZ::EntityId entity, T& source) + { + AZ::Entity* target = Internal::FindEntity(entity, source); + AZ_Assert( + target, "SpawnbleUtils were unable to locate entity with id %zu in Instance or Spawnable for cloning.", + aznumeric_cast(entity)); + auto clone = AZStd::make_unique(); + + static AZ::SerializeContext* sc = GetSerializeContext(); + sc->CloneObjectInplace(*clone, target); + clone->SetId(AZ::Entity::MakeId()); + + return clone; + } + + AZStd::unique_ptr ReplaceEntityWithPlaceholder(AZ::EntityId entity, AzToolsFramework::Prefab::Instance& source) + { + auto&& [instance, alias] = source.FindInstanceAndAlias(entity); + AZ_Assert( + instance, "SpawnbleUtils were unable to locate entity alias with id %zu in Instance '%s' for replacing.", + aznumeric_cast(entity), source.GetTemplateSourcePath().c_str()); + + EntityOptionalReference entityData = instance->GetEntity(alias); + AZ_Assert( + entityData.has_value(), "SpawnbleUtils were unable to locate entity '%.*s' in Instance '%s' for replacing.", + AZ_STRING_ARG(alias), source.GetTemplateSourcePath().c_str()); + auto placeholder = AZStd::make_unique(entityData->get().GetId(), entityData->get().GetName()); + return instance->ReplaceEntity(AZStd::move(placeholder), alias); + } + + AZStd::unique_ptr ReplaceEntityWithPlaceholder(AZ::EntityId entity, AzFramework::Spawnable& source) + { + uint32_t index = AzToolsFramework::Prefab::SpawnableUtils::FindEntityIndex(entity, source); + AZ_Assert( + index != InvalidEntityIndex, "SpawnbleUtils were unable to locate entity alias with id %zu in Spawnable for replacing.", + aznumeric_cast(entity)); + + AZStd::unique_ptr original = AZStd::move(source.GetEntities()[index]); + AZ_Assert( + original, "SpawnbleUtils were unable to locate entity with id %zu in Spawnable for replacing.", + aznumeric_cast(entity)); + + source.GetEntities()[index] = AZStd::make_unique(original->GetId(), original->GetName()); + + return original; + } + + template + AZStd::pair, AzFramework::Spawnable::EntityAliasType> ApplyAlias( + Source& source, AZ::EntityId entity, AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasType aliasType) + { + namespace PCU = AzToolsFramework::Prefab::PrefabConversionUtils; + using ResultPair = AZStd::pair, AzFramework::Spawnable::EntityAliasType>; + + switch (aliasType) + { + case PCU::EntityAliasType::Disabled: + // No need to do anything as the alias is disabled. + return ResultPair(nullptr, AzFramework::Spawnable::EntityAliasType::Disabled); + case PCU::EntityAliasType::OptionalReplace: + return ResultPair(CloneEntity(entity, source), AzFramework::Spawnable::EntityAliasType::Replace); + case PCU::EntityAliasType::Replace: + return ResultPair(ReplaceEntityWithPlaceholder(entity, source), AzFramework::Spawnable::EntityAliasType::Replace); + case PCU::EntityAliasType::Additional: + ResultPair(AZStd::make_unique(AZ::Entity::MakeId()), AzFramework::Spawnable::EntityAliasType::Additional); + case PCU::EntityAliasType::Merge: + // Use the same entity id as the original entity so at runtime the entity ids can be verified to match. + ResultPair(AZStd::make_unique(entity), AzFramework::Spawnable::EntityAliasType::Merge); + default: + AZ_Assert( + false, "Invalid PrefabProcessorContext::EntityAliasType type (%i) provided.", aznumeric_cast(aliasType)); + return ResultPair(nullptr, AzFramework::Spawnable::EntityAliasType::Disabled); + } + } + } + bool CreateSpawnable(AzFramework::Spawnable& spawnable, const PrefabDom& prefabDom) { AZStd::vector> referencedAssets; return CreateSpawnable(spawnable, prefabDom, referencedAssets); } - bool CreateSpawnable(AzFramework::Spawnable& spawnable, const PrefabDom& prefabDom, AZStd::vector>& referencedAssets) + bool CreateSpawnable( + AzFramework::Spawnable& spawnable, + const PrefabDom& prefabDom, + AZStd::vector>& referencedAssets) { Instance instance; - if (Prefab::PrefabDomUtils::LoadInstanceFromPrefabDom(instance, prefabDom, referencedAssets, - Prefab::PrefabDomUtils::LoadFlags::AssignRandomEntityId)) // Always assign random entity ids because the spawnable is - // going to be used to create clones of the entities. + if (Prefab::PrefabDomUtils::LoadInstanceFromPrefabDom( + instance, prefabDom, referencedAssets, + Prefab::PrefabDomUtils::LoadFlags::AssignRandomEntityId)) // Always assign random entity ids because the spawnable is + // going to be used to create clones of the entities. { AzFramework::Spawnable::EntityList& entities = spawnable.GetEntities(); instance.DetachAllEntitiesInHierarchy( @@ -47,6 +165,122 @@ namespace AzToolsFramework::Prefab::SpawnableUtils } } + AZ::Entity* CreateEntityAlias( + AZStd::string sourcePrefabName, + AzToolsFramework::Prefab::Instance& source, + AZStd::string targetPrefabName, + AzToolsFramework::Prefab::Instance& target, + AZ::EntityId entity, + AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasType aliasType, + AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasSpawnableLoadBehavior loadBehavior, + uint32_t tag, + AzToolsFramework::Prefab::PrefabConversionUtils::PrefabProcessorContext& context) + { + using namespace AzToolsFramework::Prefab::PrefabConversionUtils; + + AliasPath alias = source.GetAliasPathRelativeToInstance(entity); + if (!alias.empty()) + { + auto&& [replacement, storedAliasType] = Internal::ApplyAlias(source, entity, aliasType); + + AZ::Entity* result = replacement.get(); + target.AddEntity(AZStd::move(replacement), alias.Filename().Native()); + + EntityAliasStore store; + store.m_aliasType = storedAliasType; + store.m_source.emplace(AZStd::move(sourcePrefabName), AZStd::move(alias)); + store.m_target.emplace( + AZStd::move(targetPrefabName), target.GetAliasPathRelativeToInstance(result->GetId())); + store.m_loadBehavior = loadBehavior; + store.m_tag = tag; + context.RegisterSpawnableEntityAlias(AZStd::move(store)); + + return result; + } + else + { + AZ_Assert(false, "Entity with id %zu was not found in the source prefab.", static_cast(entity)); + return nullptr; + } + } + + AZ::Entity* CreateEntityAlias( + AZStd::string sourcePrefabName, + AzToolsFramework::Prefab::Instance& source, + AzFramework::Spawnable& target, + AZ::EntityId entity, + AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasType aliasType, + AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasSpawnableLoadBehavior loadBehavior, + uint32_t tag, + AzToolsFramework::Prefab::PrefabConversionUtils::PrefabProcessorContext& context) + { + using namespace AzToolsFramework::Prefab::PrefabConversionUtils; + + AliasPath alias = source.GetAliasPathRelativeToInstance(entity); + if (!alias.empty()) + { + auto&& [replacement, storedAliasType] = Internal::ApplyAlias(source, entity, aliasType); + + AZ::Entity* result = replacement.get(); + target.GetEntities().push_back(AZStd::move(replacement)); + + EntityAliasStore store; + store.m_aliasType = storedAliasType; + store.m_source.emplace(AZStd::move(sourcePrefabName), AZStd::move(alias)); + store.m_target.emplace(target, result->GetId()); + store.m_tag = tag; + store.m_loadBehavior = loadBehavior; + context.RegisterSpawnableEntityAlias(AZStd::move(store)); + + return result; + } + else + { + AZ_Assert(false, "Entity with id %zu was not found in the source prefab.", static_cast(entity)); + return nullptr; + } + } + + AZ::Entity* CreateEntityAlias( + AzFramework::Spawnable& source, + AzFramework::Spawnable& target, + AZ::EntityId entity, + AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasType aliasType, + AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasSpawnableLoadBehavior loadBehavior, + uint32_t tag, + AzToolsFramework::Prefab::PrefabConversionUtils::PrefabProcessorContext& context) + { + using namespace AzToolsFramework::Prefab::PrefabConversionUtils; + + auto&& [replacement, storedAliasType] = Internal::ApplyAlias(source, entity, aliasType); + AZ::Entity* result = replacement.get(); + target.GetEntities().push_back(AZStd::move(replacement)); + + EntityAliasStore store; + store.m_aliasType = storedAliasType; + store.m_source.emplace(source, entity); + store.m_target.emplace(target, result->GetId()); + store.m_tag = tag; + store.m_loadBehavior = loadBehavior; + context.RegisterSpawnableEntityAlias(AZStd::move(store)); + + return result; + } + + uint32_t FindEntityIndex(AZ::EntityId entity, const AzFramework::Spawnable& spawnable) + { + auto begin = spawnable.GetEntities().begin(); + auto end = spawnable.GetEntities().end(); + for(auto it = begin; it != end; ++it) + { + if ((*it)->GetId() == entity) + { + return AZStd::distance(begin, it); + } + } + return InvalidEntityIndex; + } + template void OrganizeEntitiesForSorting( AZStd::vector& entities, diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.h index ffcf13e081..892b83455d 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.h @@ -8,14 +8,59 @@ #pragma once +#include +#include #include #include +#include + +namespace AZ +{ + class Entity; +} + +namespace AzToolsFramework::Prefab +{ + class Instance; +} namespace AzToolsFramework::Prefab::SpawnableUtils { + static constexpr uint32_t InvalidEntityIndex = AZStd::numeric_limits::max(); + bool CreateSpawnable(AzFramework::Spawnable& spawnable, const PrefabDom& prefabDom); bool CreateSpawnable(AzFramework::Spawnable& spawnable, const PrefabDom& prefabDom, AZStd::vector>& referencedAssets); + AZ::Entity* CreateEntityAlias( + AZStd::string sourcePrefabName, + AzToolsFramework::Prefab::Instance& source, + AZStd::string targetPrefabName, + AzToolsFramework::Prefab::Instance& target, + AZ::EntityId entity, + AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasType aliasType, + AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasSpawnableLoadBehavior loadBehavior, + uint32_t tag, + AzToolsFramework::Prefab::PrefabConversionUtils::PrefabProcessorContext& context); + AZ::Entity* CreateEntityAlias( + AZStd::string sourcePrefabName, + AzToolsFramework::Prefab::Instance& source, + AzFramework::Spawnable& target, + AZ::EntityId entity, + AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasType aliasType, + AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasSpawnableLoadBehavior loadBehavior, + uint32_t tag, + AzToolsFramework::Prefab::PrefabConversionUtils::PrefabProcessorContext& context); + AZ::Entity* CreateEntityAlias( + AzFramework::Spawnable& source, + AzFramework::Spawnable& target, + AZ::EntityId entity, + AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasType aliasType, + AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasSpawnableLoadBehavior loadBehavior, + uint32_t tag, + AzToolsFramework::Prefab::PrefabConversionUtils::PrefabProcessorContext& context); + + uint32_t FindEntityIndex(AZ::EntityId entity, const AzFramework::Spawnable& spawnable); + void SortEntitiesByTransformHierarchy(AzFramework::Spawnable& spawnable); template diff --git a/Gems/Prefab/PrefabBuilder/PrefabBuilderComponent.cpp b/Gems/Prefab/PrefabBuilder/PrefabBuilderComponent.cpp index a86d7b57e2..6107226783 100644 --- a/Gems/Prefab/PrefabBuilder/PrefabBuilderComponent.cpp +++ b/Gems/Prefab/PrefabBuilder/PrefabBuilderComponent.cpp @@ -175,6 +175,8 @@ namespace AZ::Prefab const AzToolsFramework::Prefab::PrefabConversionUtils::PrefabProcessorContext::ProductAssetDependencyContainer& registeredDependencies, AZStd::vector& outputProducts) const { + using namespace AzToolsFramework::Prefab::PrefabConversionUtils; + outputProducts.reserve(store.size()); AZStd::vector data; @@ -211,17 +213,14 @@ namespace AZ::Prefab if (AssetBuilderSDK::OutputObject(&object.GetAsset(), object.GetAssetType(), productPath.String(), object.GetAssetType(), object.GetAsset().GetId().m_subId, product)) { - auto findRegisteredDependencies = registeredDependencies.find(object.GetAsset().GetId()); - if (findRegisteredDependencies != registeredDependencies.end()) - { - AZStd::transform(findRegisteredDependencies->second.begin(), findRegisteredDependencies->second.end(), - AZStd::back_inserter(product.m_dependencies), - [](const AZ::Data::AssetId& productId) -> AssetBuilderSDK::ProductDependency - { - return AssetBuilderSDK::ProductDependency(productId, - AZ::Data::ProductDependencyInfo::CreateFlags(AZ::Data::AssetLoadBehavior::NoLoad)); - }); - } + auto range = registeredDependencies.equal_range(object.GetAsset().GetId()); + AZStd::transform(range.first, range.second, + AZStd::back_inserter(product.m_dependencies), + [](const auto& dependency) -> AssetBuilderSDK::ProductDependency + { + return AssetBuilderSDK::ProductDependency( + dependency.second.m_assetId, AZ::Data::ProductDependencyInfo::CreateFlags(dependency.second.m_loadBehavior)); + }); outputProducts.push_back(AZStd::move(product)); } From 15ea380d3988bd79471c38a2e7524f7a3934bdb1 Mon Sep 17 00:00:00 2001 From: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> Date: Tue, 26 Oct 2021 10:43:27 -0700 Subject: [PATCH 05/14] Post integration fixes and additional changes for entity aliases in spawnables. Signed-off-by: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> --- .../AzFramework/Spawnable/Spawnable.cpp | 43 ++++++------------- .../AzFramework/Spawnable/Spawnable.h | 17 +++----- .../Spawnable/SpawnableEntitiesManager.cpp | 7 --- 3 files changed, 18 insertions(+), 49 deletions(-) diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp index 92728a4575..48662fae99 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp @@ -6,6 +6,7 @@ * */ +#include #include #include #include @@ -138,9 +139,9 @@ namespace AzFramework Optimize(); AZ_Assert( - m_owner.m_lockState == LockState::Locked, "Attempting to unlock a spawnable that's not in the locked state (%i).", - m_owner.m_lockState.load()); - m_owner.m_lockState = LockState::Unlocked; + m_owner.m_shareState == ShareState::ReadWrite, "Attempting to unlock a spawnable that's not in the locked state (%i).", + m_owner.m_shareState.load()); + m_owner.m_shareState = ShareState::NotShared; } } @@ -397,9 +398,9 @@ namespace AzFramework if (HasLock()) { AZ_Assert( - m_owner.m_lockState < 0, "Attempting to unlock a read shared spawnable that was not in a read shared mode (%i).", - m_owner.m_lockState.load()); - m_owner.m_lockState++; + m_owner.m_shareState <= ShareState::Read, "Attempting to unlock a read shared spawnable that was not in a read shared mode (%i).", + m_owner.m_shareState.load()); + m_owner.m_shareState++; } } @@ -471,15 +472,15 @@ namespace AzFramework auto Spawnable::TryGetAliasesConst() const -> EntityAliasConstVisitor { - int32_t expected = LockState::Unlocked; + int32_t expected = ShareState::NotShared; do { // Try to set the lock to a negative number to indicate a shared read. - if (m_lockState.compare_exchange_strong(expected, expected - 1)) + if (m_shareState.compare_exchange_strong(expected, expected - 1)) { return EntityAliasConstVisitor(*this, &m_entityAliases); } - // as long as the value is negative keep trying to get a shared read lock. + // as long as the value is negative or not shared then keep trying to get a shared read lock. } while (expected <= 0); return EntityAliasConstVisitor(*this, nullptr); } @@ -491,9 +492,9 @@ namespace AzFramework auto Spawnable::TryGetAliases() -> EntityAliasVisitor { - int32_t expected = LockState::Unlocked; - return m_lockState.compare_exchange_strong(expected, LockState::Locked) ? EntityAliasVisitor(*this, &m_entityAliases) - : EntityAliasVisitor(*this, nullptr); + int32_t expected = ShareState::NotShared; + return m_shareState.compare_exchange_strong(expected, ShareState::ReadWrite) ? EntityAliasVisitor(*this, &m_entityAliases) + : EntityAliasVisitor(*this, nullptr); } bool Spawnable::IsEmpty() const @@ -501,24 +502,6 @@ namespace AzFramework return m_entities.empty(); } - bool Spawnable::IsPermanentlyLocked() const - { - return m_lockState == LockState::PermanentLock; - } - - bool Spawnable::LockPermanently() - { - if (!IsPermanentlyLocked()) - { - int32_t expected = LockState::Unlocked; - return m_lockState.compare_exchange_strong(expected, LockState::PermanentLock); - } - else - { - return true; - } - } - SpawnableMetaData& Spawnable::GetMetaData() { return m_metaData; diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h index 05ae10e580..4fb1f84b6f 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h @@ -41,11 +41,11 @@ namespace AzFramework //!< maintaining a valid component list. }; - enum LockState : int32_t + enum ShareState : int32_t { - Unlocked, - Locked, - PermanentLock + Read = -1, + NotShared = 0, + ReadWrite = 1 }; //! An entity alias redirects the spawning of an entity to another entity, possibly in another spawnable. @@ -183,13 +183,6 @@ namespace AzFramework EntityAliasVisitor TryGetAliases(); bool IsEmpty() const; - //! Whether or not the spawnable is permanently locked. If so then parts of the spawnable can no longer be modified. - bool IsPermanentlyLocked() const; - //! Permanently locks access to parts of the spawnable from being modified. - //! @return True if the spawnable could be locked. If false is returned another operation is still making modifications. In this case - //! call this again at a later point in time. - bool LockPermanently(); - SpawnableMetaData& GetMetaData(); const SpawnableMetaData& GetMetaData() const; @@ -204,6 +197,6 @@ namespace AzFramework // Includes both direct and nested entities of the prefab. EntityList m_entities; - mutable AZStd::atomic m_lockState{ LockState::Unlocked }; + mutable AZStd::atomic m_shareState{ ShareState::NotShared }; }; } // namespace AzFramework diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp index 0ac80c3ed6..22918b7173 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp @@ -857,13 +857,6 @@ namespace AzFramework ticket.m_currentRequestId++; return CommandResult::Executed; } - else - { - AZ_Assert( - ticket.m_spawnable->IsPermanentlyLocked(), - "An request to UpdateEntityAliasTypes on the Spawnables Entities Manager was processed on a spawnable that's permanently " - "locked."); - } } return CommandResult::Requeue; } From 66df146554feb6cac2e841803ddabbbc06d608c2 Mon Sep 17 00:00:00 2001 From: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> Date: Tue, 26 Oct 2021 13:33:02 -0700 Subject: [PATCH 06/14] Fixed existing spawnable unit tests to work with entity alias changes. Signed-off-by: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> --- .../Spawnable/SpawnableEntitiesManager.cpp | 96 ++++++++----------- .../SpawnableEntitiesManagerTests.cpp | 10 +- 2 files changed, 43 insertions(+), 63 deletions(-) diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp index 22918b7173..a018a55210 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp @@ -303,18 +303,11 @@ namespace AzFramework AZ::Entity* SpawnableEntitiesManager::CloneSingleEntity(const AZ::Entity& entityTemplate, EntityIdMap& templateToCloneMap, AZ::SerializeContext& serializeContext) { - if (!entityTemplate.GetComponents().empty()) - { - // If the same ID gets remapped more than once, preserve the original remapping instead of overwriting it. - constexpr bool allowDuplicateIds = false; + // If the same ID gets remapped more than once, preserve the original remapping instead of overwriting it. + constexpr bool allowDuplicateIds = false; - return AZ::IdUtils::Remapper::CloneObjectAndGenerateNewIdsAndFixRefs( - &entityTemplate, templateToCloneMap, &serializeContext); - } - else - { - return nullptr; - } + return AZ::IdUtils::Remapper::CloneObjectAndGenerateNewIdsAndFixRefs( + &entityTemplate, templateToCloneMap, &serializeContext); } AZ::Entity* SpawnableEntitiesManager::CloneSingleAliasedEntity( @@ -468,9 +461,8 @@ namespace AzFramework if (aliasIt == aliasEnd || aliasIt->m_sourceIndex != i) { - AZ::Entity* clone = - CloneSingleEntity(*entitiesToSpawn[i], ticket.m_entityIdReferenceMap, *request.m_serializeContext); - spawnedEntities.emplace_back(clone); + spawnedEntities.emplace_back( + CloneSingleEntity(*entitiesToSpawn[i], ticket.m_entityIdReferenceMap, *request.m_serializeContext)); spawnedEntityIndices.push_back(i); } else @@ -483,21 +475,9 @@ namespace AzFramework AZ::Entity* clone = CloneSingleAliasedEntity( *entitiesToSpawn[i], *aliasIt, ticket.m_entityIdReferenceMap, previousEntity, *request.m_serializeContext); - // Not all alias operations create a new instance. It's also possible for an empty entity to be left behind, - // in which case it's also filtered out as the entity component framework doesn't handle these gracefully. - if (clone) - { - if (!clone->GetComponents().empty()) - { - previousEntity = clone; - spawnedEntities.emplace_back(clone); - spawnedEntityIndices.push_back(i); - } - else - { - delete clone; - } - } + previousEntity = clone; + spawnedEntities.emplace_back(clone); + spawnedEntityIndices.push_back(i); ++aliasIt; } while (aliasIt != aliasEnd && aliasIt->m_sourceIndex == i); } @@ -519,8 +499,13 @@ namespace AzFramework // Add to the game context, now the entities are active for (auto it = newEntitiesBegin; it != newEntitiesEnd; ++it) { - (*it)->SetSpawnTicketId(request.m_ticketId); - GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, *it); + AZ::Entity* clone = (*it); + // The entity component framework doesn't handle entities without TransformComponent safely. + if (!clone->GetComponents().empty()) + { + clone->SetSpawnTicketId(request.m_ticketId); + 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. @@ -585,10 +570,8 @@ namespace AzFramework RefreshEntityIdMapping( entitiesToSpawn[index].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned); - AZ::Entity* clone = - CloneSingleEntity(*entitiesToSpawn[index], ticket.m_entityIdReferenceMap, *request.m_serializeContext); - AZ_Assert(clone != nullptr, "Failed to clone spawnable entity."); - spawnedEntities.push_back(clone); + spawnedEntities.push_back( + CloneSingleEntity(*entitiesToSpawn[index], ticket.m_entityIdReferenceMap, *request.m_serializeContext)); spawnedEntityIndices.push_back(index); } } @@ -612,9 +595,8 @@ namespace AzFramework if (aliasIt == aliasEnd) { - AZ::Entity* clone = - CloneSingleEntity(*entitiesToSpawn[index], ticket.m_entityIdReferenceMap, *request.m_serializeContext); - spawnedEntities.emplace_back(clone); + spawnedEntities.emplace_back( + CloneSingleEntity(*entitiesToSpawn[index], ticket.m_entityIdReferenceMap, *request.m_serializeContext)); spawnedEntityIndices.push_back(index); } else @@ -627,22 +609,10 @@ namespace AzFramework AZ::Entity* clone = CloneSingleAliasedEntity( *entitiesToSpawn[index], *aliasIt, ticket.m_entityIdReferenceMap, previousEntity, *request.m_serializeContext); - // Not all alias operations create a new instance. It's also possible for an empty entity to be left - // behind, in which case it's also filtered out as the entity component framework doesn't handle these - // gracefully. - if (clone) - { - if (!clone->GetComponents().empty()) - { - previousEntity = clone; - spawnedEntities.emplace_back(clone); - spawnedEntityIndices.push_back(index); - } - else - { - delete clone; - } - } + previousEntity = clone; + spawnedEntities.emplace_back(clone); + spawnedEntityIndices.push_back(index); + ++aliasIt; } while (aliasIt != aliasEnd && aliasIt->m_sourceIndex == index); } @@ -663,8 +633,13 @@ namespace AzFramework // Add to the game context, now the entities are active for (auto it = ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount; it != ticket.m_spawnedEntities.end(); ++it) { - (*it)->SetSpawnTicketId(request.m_ticketId); - GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, *it); + AZ::Entity* clone = (*it); + // The entity component framework doesn't handle entities without TransformComponent safely. + if (!clone->GetComponents().empty()) + { + clone->SetSpawnTicketId(request.m_ticketId); + GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, *it); + } } if (request.m_completionCallback) @@ -965,13 +940,18 @@ namespace AzFramework { for (AZ::Entity* entity : request.m_ticket->m_spawnedEntities) { - if (entity != nullptr) + if (entity != nullptr && !entity->GetComponents().empty()) { - // Setting it to 0 is needed to avoid the infite loop between GameEntityContext and SpawnableEntitiesManager. + // Setting it to 0 is needed to avoid the infinite loop between GameEntityContext and SpawnableEntitiesManager. entity->SetSpawnTicketId(0); GameEntityContextRequestBus::Broadcast( &GameEntityContextRequestBus::Events::DestroyGameEntity, entity->GetId()); } + else + { + // Entities without components wouldn't have been send to the GameEntityContext. + delete entity; + } } delete request.m_ticket; diff --git a/Code/Framework/AzFramework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp b/Code/Framework/AzFramework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp index 2dc32d14d5..ff48f73769 100644 --- a/Code/Framework/AzFramework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp +++ b/Code/Framework/AzFramework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp @@ -403,7 +403,7 @@ namespace UnitTest static constexpr size_t NumEntities = 4; FillSpawnable(NumEntities); - AZStd::vector indices = { 0, 2, 3, 1 }; + AZStd::vector indices = { 0, 2, 3, 1 }; size_t spawnedEntitiesCount = 0; auto callback = [&spawnedEntitiesCount](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) @@ -423,7 +423,7 @@ namespace UnitTest static constexpr size_t NumEntities = 1; FillSpawnable(NumEntities); - AZStd::vector indices = { 0, 0 }; + AZStd::vector indices = { 0, 0 }; size_t spawnedEntitiesCount = 0; auto callback = @@ -444,7 +444,7 @@ namespace UnitTest static constexpr size_t NumEntities = 4; FillSpawnable(NumEntities); - AZStd::vector indices = { 0, 2, 3, 1 }; + AZStd::vector indices = { 0, 2, 3, 1 }; size_t spawnedEntitiesCount = 0; auto callback = @@ -467,7 +467,7 @@ namespace UnitTest FillSpawnable(NumEntities); CreateSingleParent(); - AZStd::vector indices = { 0, 1, 2, 3 }; + AZStd::vector indices = { 0, 1, 2, 3 }; AZStd::vector parents; auto callback = [&parents](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) @@ -499,7 +499,7 @@ namespace UnitTest FillSpawnable(NumEntities); CreateSingleParent(); - AZStd::vector indices = { 0, 1, 2, 3 }; + AZStd::vector indices = { 0, 1, 2, 3 }; AZStd::vector parents; auto callback = From 6587e149b758a35b3fa62655db5e640b3b81e0ac Mon Sep 17 00:00:00 2001 From: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> Date: Thu, 28 Oct 2021 09:57:33 -0700 Subject: [PATCH 07/14] Added unit tests for spawnable entity aliases. This also fixes several issues discovered through the unit tests and renames a few functions to be clearer. Signed-off-by: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> --- .../AzFramework/Spawnable/Spawnable.cpp | 108 ++-- .../AzFramework/Spawnable/Spawnable.h | 16 +- .../Spawnable/SpawnableAssetHandler.cpp | 4 +- .../Spawnable/SpawnableEntitiesManager.cpp | 29 +- .../SpawnableEntitiesManagerTests.cpp | 479 ++++++++++++++++- .../Tests/Spawnable/SpawnableTests.cpp | 499 ++++++++++++++++++ .../Tests/frameworktests_files.cmake | 1 + .../Spawnable/PrefabProcessorContext.cpp | 2 +- .../Prefab/Spawnable/PrefabProcessorContext.h | 3 +- 9 files changed, 1057 insertions(+), 84 deletions(-) create mode 100644 Code/Framework/AzFramework/Tests/Spawnable/SpawnableTests.cpp diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp index 48662fae99..7548a8f181 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp @@ -9,7 +9,9 @@ #include #include #include +#include #include +#include #include namespace AzFramework @@ -31,7 +33,7 @@ namespace AzFramework // EntityAliasVisitorBase // - bool Spawnable::EntityAliasVisitorBase::HasLock(const EntityAliasList* aliases) const + bool Spawnable::EntityAliasVisitorBase::IsSet(const EntityAliasList* aliases) const { return aliases != nullptr; } @@ -92,11 +94,15 @@ namespace AzFramework AZStd::unordered_set spawnableIds; for (const Spawnable::EntityAlias& alias : *aliases) { - auto it = spawnableIds.find(alias.m_spawnable.GetId()); - if (it == spawnableIds.end()) + // If the spawnable id is not valid it means that the alias is referencing the spawnable it's stored on. + if (alias.m_spawnable.GetId().IsValid()) { - callback(alias.m_spawnable); - spawnableIds.emplace(alias.m_spawnable.GetId()); + auto it = spawnableIds.find(alias.m_spawnable.GetId()); + if (it == spawnableIds.end()) + { + callback(alias.m_spawnable); + spawnableIds.emplace(alias.m_spawnable.GetId()); + } } } } @@ -108,7 +114,8 @@ namespace AzFramework AZStd::unordered_set spawnableIds; for (const Spawnable::EntityAlias& alias : *aliases) { - if (alias.m_tag == tag) + // If the spawnable id is not valid it means that the alias is referencing the spawnable it's stored on. + if (alias.m_tag == tag && alias.m_spawnable.GetId().IsValid()) { auto it = spawnableIds.find(alias.m_spawnable.GetId()); if (it == spawnableIds.end()) @@ -134,7 +141,7 @@ namespace AzFramework Spawnable::EntityAliasVisitor::~EntityAliasVisitor() { - if (HasLock()) + if (IsSet()) { Optimize(); @@ -169,9 +176,9 @@ namespace AzFramework return *this; } - bool Spawnable::EntityAliasVisitor::HasLock() const + bool Spawnable::EntityAliasVisitor::IsSet() const { - return EntityAliasVisitorBase::HasLock(m_entityAliasList); + return EntityAliasVisitorBase::IsSet(m_entityAliasList); } bool Spawnable::EntityAliasVisitor::HasAliases() const @@ -235,7 +242,7 @@ namespace AzFramework m_dirty = true; } - void Spawnable::EntityAliasVisitor::ListSpawnablesPendingLoad(const ListSpawnablesPendingLoadCallback& callback) + void Spawnable::EntityAliasVisitor::ListSpawnablesRequiringLoad(const ListSpawnablesRequiringLoadCallback& callback) { AZ_Assert(m_entityAliasList, "Attempting to visit entity aliases on a spawnable that wasn't locked."); for (Spawnable::EntityAlias& alias : *m_entityAliasList) @@ -306,75 +313,92 @@ namespace AzFramework // aliases, for instance Networking can decide to disable certain aliases when running on a client. This in turn also requires // the aliases to be in their recorded order during building as the ebus handlers may depend on that order to determine what // entities need to be updated. - Spawnable::EntityAlias* compare = m_entityAliasList->begin(); - Spawnable::EntityAlias* it = m_entityAliasList->begin() + 1; + uint32_t previousIndex = AZStd::numeric_limits::max(); + Spawnable::EntityAliasType previousType = + static_cast(AZStd::numeric_limits>::max()); + Spawnable::EntityAlias* it = m_entityAliasList->begin(); Spawnable::EntityAlias* end = m_entityAliasList->end(); while (it < end) { + // If there's a switch to a new source index and the previous index only had an original it can + // be removed. + if (previousType == Spawnable::EntityAliasType::Original && previousIndex != it->m_sourceIndex) + { + it = m_entityAliasList->erase(it - 1); + end = m_entityAliasList->end(); + if (it == end) + { + break; + } + } + switch (it->m_aliasType) { case Spawnable::EntityAliasType::Original: - // If this is the only alias for the entity then the original can be removed. - { - Spawnable::EntityAlias* next = it + 1; - if (next == end || next->m_sourceIndex != it->m_sourceIndex) - { - // Erase instead of a swap-and-pop in order to preserver the order. - m_entityAliasList->erase(compare); - --end; - break; - } - } [[fallthrough]]; case Spawnable::EntityAliasType::Disabled: [[fallthrough]]; case Spawnable::EntityAliasType::Replace: // If the previous entry was a disabled, original or replace alias then remove it as it will be overwritten by the // current entry. - if (compare->m_sourceIndex == it->m_sourceIndex && - (compare->m_aliasType == Spawnable::EntityAliasType::Original || - compare->m_aliasType == Spawnable::EntityAliasType::Disabled || - compare->m_aliasType == Spawnable::EntityAliasType::Replace)) + if (previousIndex == it->m_sourceIndex && + (previousType == Spawnable::EntityAliasType::Original || + previousType == Spawnable::EntityAliasType::Disabled || + previousType == Spawnable::EntityAliasType::Replace)) { + previousIndex = it->m_sourceIndex; + previousType = it->m_aliasType; // Erase instead of a swap-and-pop in order to preserver the order. - m_entityAliasList->erase(compare); - --end; + it = m_entityAliasList->erase(it - 1) + 1; + end = m_entityAliasList->end(); } else { - ++compare; + previousIndex = it->m_sourceIndex; + previousType = it->m_aliasType; ++it; } break; case Spawnable::EntityAliasType::Additional: [[fallthrough]]; case Spawnable::EntityAliasType::Merge: - // If this is the first entry for this type insert an original in front of it so the spawnable entity manager - // does have to check for the case there's a merge and/or addition without a prefix. - if (compare->m_sourceIndex != it->m_sourceIndex) + // If this is the first entry for this index then insert an original in front of it so the spawnable entity manager + // doesn't have to check for the case there's a merge and/or addition without an entity to extend. + if (previousIndex != it->m_sourceIndex) { Spawnable::EntityAlias insert; // No load, as the asset is already loaded. - insert.m_spawnable = AZ::Data::Asset(&m_owner, AZ::Data::AssetLoadBehavior::NoLoad); + insert.m_spawnable = AZ::Data::Asset({}, azrtti_typeid()); insert.m_sourceIndex = it->m_sourceIndex; insert.m_targetIndex = it->m_sourceIndex; // Source index as the original entry for this slot is added. insert.m_aliasType = Spawnable::EntityAliasType::Original; - m_entityAliasList->insert(compare, AZStd::move(insert)); - compare += 2; + + previousIndex = it->m_sourceIndex; + previousType = it->m_aliasType; + + // Insert to maintain the order. + it = m_entityAliasList->insert(it, AZStd::move(insert)); it += 2; - ++end; + end = m_entityAliasList->end(); } else { - ++compare; + previousType = it->m_aliasType; ++it; } break; default: - AZ_Assert(false, "Invalid Spawnable entity alias type found during asset loading: %i", compare->m_aliasType); + AZ_Assert(false, "Invalid Spawnable entity alias type found during asset loading: %i", it->m_aliasType); break; } } + + // Check if the last entry is an "Original" in which case it can be removed. + if (!m_entityAliasList->empty() && m_entityAliasList->back().m_aliasType == Spawnable::EntityAliasType::Original) + { + m_entityAliasList->pop_back(); + } + // Reclaim memory because after this point the aliases will not change anymore. m_entityAliasList->shrink_to_fit(); m_dirty = false; @@ -395,7 +419,7 @@ namespace AzFramework Spawnable::EntityAliasConstVisitor::~EntityAliasConstVisitor() { - if (HasLock()) + if (IsSet()) { AZ_Assert( m_owner.m_shareState <= ShareState::Read, "Attempting to unlock a read shared spawnable that was not in a read shared mode (%i).", @@ -404,9 +428,9 @@ namespace AzFramework } } - bool Spawnable::EntityAliasConstVisitor::HasLock() const + bool Spawnable::EntityAliasConstVisitor::IsSet() const { - return EntityAliasVisitorBase::HasLock(m_entityAliasList); + return EntityAliasVisitorBase::IsSet(m_entityAliasList); } bool Spawnable::EntityAliasConstVisitor::HasAliases() const diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h index 4fb1f84b6f..0a35b81fb1 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h @@ -71,7 +71,8 @@ namespace AzFramework class EntityAliasVisitorBase { protected: - bool HasLock(const EntityAliasList* aliases) const; + bool IsSet(const EntityAliasList* aliases) const; + bool HasAliases(const EntityAliasList* aliases) const; bool AreAllSpawnablesReady(const EntityAliasList* aliases) const; @@ -98,7 +99,9 @@ namespace AzFramework EntityAliasVisitor(const EntityAliasVisitor& rhs) = delete; EntityAliasVisitor& operator=(const EntityAliasVisitor& rhs) = delete; - bool HasLock() const; + //! Checks if the visitor was able to retrieve data. This needs to be checked before calling any other functions. + bool IsSet() const; + bool HasAliases() const; bool AreAllSpawnablesReady() const; @@ -118,8 +121,8 @@ namespace AzFramework Spawnable::EntityAliasType aliasType, bool queueLoad); - using ListSpawnablesPendingLoadCallback = AZStd::function& spawnablePendingLoad)>; - void ListSpawnablesPendingLoad(const ListSpawnablesPendingLoadCallback& callback); + using ListSpawnablesRequiringLoadCallback = AZStd::function& spawnablePendingLoad)>; + void ListSpawnablesRequiringLoad(const ListSpawnablesRequiringLoadCallback& callback); using UpdateCallback = AZStd::functionTryGetAliases(); - AZ_Assert(aliases.HasLock(), "Newly created Spawnable '%s' was already locked.", asset.GetHint().c_str()); + AZ_Assert(aliases.IsSet(), "Newly created Spawnable '%s' was already locked.", asset.GetHint().c_str()); if (aliases.HasAliases()) { AZ_Assert( @@ -119,7 +119,7 @@ namespace AzFramework &SpawnableAssetEvents::OnResolveAliases, aliases, spawnable->GetMetaData(), spawnable->GetEntities()); aliases.Optimize(); - aliases.ListSpawnablesPendingLoad( + aliases.ListSpawnablesRequiringLoad( [&assetLoadFilterCB, streamingDeadline, streamingPriority](AZ::Data::Asset& assetPendingLoad) { AZ::Data::AssetLoadParameters loadInfo; diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp index a018a55210..d7d3f59154 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp @@ -342,9 +342,6 @@ namespace AzFramework return clone; case Spawnable::EntityAliasType::Merge: AZ_Assert(previouslySpawnedEntity != nullptr, "Merging components but there's no entity to add to yet."); - AZ_Assert( - previouslySpawnedEntity->GetId() == alias.m_spawnable->GetEntities()[alias.m_targetIndex]->GetId(), - "Entity ids for merging spawnables don't match."); AppendComponents( *previouslySpawnedEntity, alias.m_spawnable->GetEntities()[alias.m_targetIndex]->GetComponents(), templateToCloneMap, serializeContext); return nullptr; @@ -412,7 +409,7 @@ namespace AzFramework if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId) { if (Spawnable::EntityAliasConstVisitor aliases = ticket.m_spawnable->TryGetAliasesConst(); - aliases.HasLock() && aliases.AreAllSpawnablesReady()) + aliases.IsSet() && aliases.AreAllSpawnablesReady()) { AZStd::vector& spawnedEntities = ticket.m_spawnedEntities; AZStd::vector& spawnedEntityIndices = ticket.m_spawnedEntityIndices; @@ -475,9 +472,12 @@ namespace AzFramework AZ::Entity* clone = CloneSingleAliasedEntity( *entitiesToSpawn[i], *aliasIt, ticket.m_entityIdReferenceMap, previousEntity, *request.m_serializeContext); - previousEntity = clone; - spawnedEntities.emplace_back(clone); - spawnedEntityIndices.push_back(i); + previousEntity = clone; + if (clone) + { + spawnedEntities.emplace_back(clone); + spawnedEntityIndices.push_back(i); + } ++aliasIt; } while (aliasIt != aliasEnd && aliasIt->m_sourceIndex == i); } @@ -527,7 +527,7 @@ namespace AzFramework if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId) { if (Spawnable::EntityAliasConstVisitor aliases = ticket.m_spawnable->TryGetAliasesConst(); - aliases.HasLock() && aliases.AreAllSpawnablesReady()) + aliases.IsSet() && aliases.AreAllSpawnablesReady()) { AZStd::vector& spawnedEntities = ticket.m_spawnedEntities; AZStd::vector& spawnedEntityIndices = ticket.m_spawnedEntityIndices; @@ -593,7 +593,7 @@ namespace AzFramework return lhs.m_sourceIndex < rhs; }); - if (aliasIt == aliasEnd) + if (aliasIt == aliasEnd || aliasIt->m_sourceIndex != index) { spawnedEntities.emplace_back( CloneSingleEntity(*entitiesToSpawn[index], ticket.m_entityIdReferenceMap, *request.m_serializeContext)); @@ -610,8 +610,11 @@ namespace AzFramework *entitiesToSpawn[index], *aliasIt, ticket.m_entityIdReferenceMap, previousEntity, *request.m_serializeContext); previousEntity = clone; - spawnedEntities.emplace_back(clone); - spawnedEntityIndices.push_back(index); + if (clone) + { + spawnedEntities.emplace_back(clone); + spawnedEntityIndices.push_back(index); + } ++aliasIt; } while (aliasIt != aliasEnd && aliasIt->m_sourceIndex == index); @@ -816,7 +819,7 @@ namespace AzFramework Ticket& ticket = *request.m_ticket; if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId) { - if (Spawnable::EntityAliasVisitor aliases = ticket.m_spawnable->TryGetAliases(); aliases.HasLock()) + if (Spawnable::EntityAliasVisitor aliases = ticket.m_spawnable->TryGetAliases(); aliases.IsSet()) { for (EntityAliasTypeChange& replacement : request.m_entityAliases) { @@ -918,7 +921,7 @@ namespace AzFramework if (request.m_checkAliasSpawnables) { if (Spawnable::EntityAliasConstVisitor visitor = ticket.m_spawnable->TryGetAliasesConst(); - !visitor.HasLock() || !visitor.AreAllSpawnablesReady()) + !visitor.IsSet() || !visitor.AreAllSpawnablesReady()) { return CommandResult::Requeue; } diff --git a/Code/Framework/AzFramework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp b/Code/Framework/AzFramework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp index ff48f73769..38847edc07 100644 --- a/Code/Framework/AzFramework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp +++ b/Code/Framework/AzFramework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp @@ -55,6 +55,40 @@ namespace UnitTest AZ::EntityId m_entityReference; }; + class SourceSpawnableComponent : public AZ::Component + { + public: + AZ_COMPONENT(SourceSpawnableComponent, "{47FF79CE-A95B-420E-8BEB-F1CC58087B87}"); + + void Activate() override {} + void Deactivate() override {} + + static void Reflect(AZ::ReflectContext* reflection) + { + if (auto* serializeContext = azrtti_cast(reflection)) + { + serializeContext->Class(); + } + } + }; + + class TargetSpawnableComponent : public AZ::Component + { + public: + AZ_COMPONENT(TargetSpawnableComponent, "{B4041561-63A7-4E1E-80F1-78C08D497960}"); + + void Activate() override {} + void Deactivate() override {} + + static void Reflect(AZ::ReflectContext* reflection) + { + if (auto* serializeContext = azrtti_cast(reflection)) + { + serializeContext->Class(); + } + } + }; + class SpawnableEntitiesManagerTest : public AllocatorsFixture { public: @@ -66,6 +100,8 @@ namespace UnitTest AZ::ComponentApplication::Descriptor descriptor; m_application->Start(descriptor); m_application->RegisterComponentDescriptor(ComponentWithEntityReference::CreateDescriptor()); + m_application->RegisterComponentDescriptor(SourceSpawnableComponent::CreateDescriptor()); + m_application->RegisterComponentDescriptor(TargetSpawnableComponent::CreateDescriptor()); // Without this, the user settings component would attempt to save on finalize/shutdown. Since the file is // shared across the whole engine, if multiple tests are run in parallel, the saving could cause a crash @@ -109,7 +145,50 @@ namespace UnitTest entities.reserve(numElements); for (size_t i=0; i()); + auto entry = AZStd::make_unique(); + entry->AddComponent(aznew SourceSpawnableComponent()); + entities.push_back(AZStd::move(entry)); + } + } + + AZ::Data::Asset CreateTargetSpawnable(size_t numElements) + { + auto target = aznew AzFramework::Spawnable( + AZ::Data::AssetId(AZ::Uuid("{716CD8C3-0BA8-4F32-B579-0EC7C967796F}")), AZ::Data::AssetData::AssetStatus::Ready); + + AzFramework::Spawnable::EntityList& entities = target->GetEntities(); + entities.reserve(numElements); + for (size_t i = 0; i < numElements; ++i) + { + auto entry = AZStd::make_unique(); + entry->AddComponent(aznew TargetSpawnableComponent()); + entities.push_back(AZStd::move(entry)); + } + + return AZ::Data::Asset(target, AZ::Data::AssetLoadBehavior::NoLoad); + } + + template + void InsertEntityAliases( + const AZStd::array& sourceIds, + const AZStd::array& targetIds, + const AZStd::array& aliasTypes, + AZ::Data::Asset* target = nullptr) + { + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + + for (uint32_t i = 0; i < AliasCount; ++i) + { + if (target) + { + visitor.AddAlias(*target, AZ::Crc32(i), sourceIds[i], targetIds[i], aliasTypes[i], false); + } + else + { + AZ::Data::Asset spawnable( + AZ::Data::AssetId(AZ::Uuid("{4CBEC17A-52D6-42D5-9037-F4C05B9CE1D9}"), i), azrtti_typeid()); + visitor.AddAlias(AZStd::move(spawnable), AZ::Crc32(i), sourceIds[i], targetIds[i], aliasTypes[i], false); + } } } @@ -245,6 +324,30 @@ namespace UnitTest TestApplication* m_application { nullptr }; }; + + // + // Constructors + // + + TEST_F(SpawnableEntitiesManagerTest, EntitySpawnTicket_Move_Works) + { + AzFramework::EntitySpawnTicket ticket1(*m_spawnableAsset); + AzFramework::EntitySpawnTicket ticket2(*m_spawnableAsset); + + const AzFramework::EntitySpawnTicket::Id ticket1Id = ticket1.GetId(); + const AzFramework::EntitySpawnTicket::Id ticket2Id = ticket2.GetId(); + + AzFramework::EntitySpawnTicket ticketMoveConstructor(AZStd::move(ticket1)); + EXPECT_TRUE(ticketMoveConstructor.IsValid()); + EXPECT_EQ(ticketMoveConstructor.GetId(), ticket1Id); + + AzFramework::EntitySpawnTicket ticketMoveOperator; + ticketMoveOperator = AZStd::move(ticket2); + EXPECT_TRUE(ticketMoveOperator.IsValid()); + EXPECT_EQ(ticketMoveOperator.GetId(), ticket2Id); + } + + // // SpawnAllEntitities // @@ -366,24 +469,6 @@ namespace UnitTest } } - TEST_F(SpawnableEntitiesManagerTest, EntitySpawnTicket_Move_Works) - { - AzFramework::EntitySpawnTicket ticket1(*m_spawnableAsset); - AzFramework::EntitySpawnTicket ticket2(*m_spawnableAsset); - - const AzFramework::EntitySpawnTicket::Id ticket1Id = ticket1.GetId(); - const AzFramework::EntitySpawnTicket::Id ticket2Id = ticket2.GetId(); - - AzFramework::EntitySpawnTicket ticketMoveConstructor(AZStd::move(ticket1)); - EXPECT_TRUE(ticketMoveConstructor.IsValid()); - EXPECT_EQ(ticketMoveConstructor.GetId(), ticket1Id); - - AzFramework::EntitySpawnTicket ticketMoveOperator; - ticketMoveOperator = AZStd::move(ticket2); - EXPECT_TRUE(ticketMoveOperator.IsValid()); - EXPECT_EQ(ticketMoveOperator.GetId(), ticket2Id); - } - TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_DeleteTicketBeforeCall_NoCrash) { { @@ -393,6 +478,178 @@ namespace UnitTest m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); } + TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_AllAliasesWithDisabled_NoEntitiesSpawned) + { + using namespace AzFramework; + static constexpr size_t NumEntities = 4; + FillSpawnable(NumEntities); + InsertEntityAliases( + { 0, 1, 2, 3 }, { 0, 1, 2, 3 }, + { Spawnable::EntityAliasType::Disabled, Spawnable::EntityAliasType::Disabled, Spawnable::EntityAliasType::Disabled, + Spawnable::EntityAliasType::Disabled }); + + size_t spawnedEntitiesCount = 0; + auto callback = [&spawnedEntitiesCount](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) + { + spawnedEntitiesCount += entities.size(); + }; + AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs; + optionalArgs.m_completionCallback = AZStd::move(callback); + m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs)); + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + + EXPECT_EQ(0, spawnedEntitiesCount); + } + + TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_SomeAliasesWithDisabled_RegularEntitiesAreSpawned) + { + using namespace AzFramework; + static constexpr size_t NumEntities = 8; + FillSpawnable(NumEntities); + InsertEntityAliases<2>({ 1, 3 }, { 1, 3 }, { Spawnable::EntityAliasType::Disabled, Spawnable::EntityAliasType::Disabled }); + + size_t spawnedEntitiesCount = 0; + auto callback = [&spawnedEntitiesCount](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) + { + spawnedEntitiesCount += entities.size(); + }; + AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs; + optionalArgs.m_completionCallback = AZStd::move(callback); + m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs)); + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + + EXPECT_EQ(6, spawnedEntitiesCount); + } + + TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_AllAliasesWithReplace_EntitiesSpawnedFromTarget) + { + using namespace AzFramework; + static constexpr size_t NumEntities = 4; + FillSpawnable(NumEntities); + AZ::Data::Asset target = CreateTargetSpawnable(4); + InsertEntityAliases<4>( + { 0, 1, 2, 3 }, { 0, 1, 2, 3 }, + { Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, + Spawnable::EntityAliasType::Replace }, + &target); + + size_t spawnedEntitiesCount = 0; + bool allReplaced = true; + auto callback = [&spawnedEntitiesCount, &allReplaced]( + AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) + { + spawnedEntitiesCount += entities.size(); + for (const AZ::Entity* entity : entities) + { + if (entity) + { + allReplaced = allReplaced && entity->FindComponent() == nullptr; + allReplaced = allReplaced && entity->FindComponent() != nullptr; + } + else + { + allReplaced = false; + } + } + }; + AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs; + optionalArgs.m_completionCallback = AZStd::move(callback); + m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs)); + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + + EXPECT_EQ(4, spawnedEntitiesCount); + EXPECT_TRUE(allReplaced); + } + + TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_AllAliasesWithAdditional_SourceAndTargetComponentsMerged) + { + using namespace AzFramework; + static constexpr size_t NumEntities = 4; + FillSpawnable(NumEntities); + AZ::Data::Asset target = CreateTargetSpawnable(4); + InsertEntityAliases<4>( + { 0, 1, 2, 3 }, { 0, 1, 2, 3 }, + { Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional, + Spawnable::EntityAliasType::Additional }, + &target); + + size_t spawnedEntitiesCount = 0; + bool allReplaced = true; + auto callback = [&spawnedEntitiesCount, &allReplaced]( + AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) + { + spawnedEntitiesCount += entities.size(); + bool onSource = true; + for (const AZ::Entity* entity : entities) + { + if (entity) + { + if (onSource) + { + allReplaced = allReplaced && entity->FindComponent() != nullptr; + allReplaced = allReplaced && entity->FindComponent() == nullptr; + } + else + { + allReplaced = allReplaced && entity->FindComponent() == nullptr; + allReplaced = allReplaced && entity->FindComponent() != nullptr; + } + onSource = !onSource; + } + else + { + allReplaced = false; + } + } + }; + AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs; + optionalArgs.m_completionCallback = AZStd::move(callback); + m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs)); + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + + EXPECT_EQ(8, spawnedEntitiesCount); + EXPECT_TRUE(allReplaced); + } + + TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_AllAliasesWithMerge_SourceAndTargetComponentsMerged) + { + using namespace AzFramework; + static constexpr size_t NumEntities = 4; + FillSpawnable(NumEntities); + AZ::Data::Asset target = CreateTargetSpawnable(4); + InsertEntityAliases<4>( + { 0, 1, 2, 3 }, { 0, 1, 2, 3 }, + { Spawnable::EntityAliasType::Merge, Spawnable::EntityAliasType::Merge, Spawnable::EntityAliasType::Merge, + Spawnable::EntityAliasType::Merge }, + &target); + + size_t spawnedEntitiesCount = 0; + bool allReplaced = true; + auto callback = [&spawnedEntitiesCount, &allReplaced]( + AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) + { + spawnedEntitiesCount += entities.size(); + for (const AZ::Entity* entity : entities) + { + if (entity) + { + allReplaced = allReplaced && entity->FindComponent() != nullptr; + allReplaced = allReplaced && entity->FindComponent() != nullptr; + } + else + { + allReplaced = false; + } + } + }; + AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs; + optionalArgs.m_completionCallback = AZStd::move(callback); + m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs)); + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + + EXPECT_EQ(4, spawnedEntitiesCount); + EXPECT_TRUE(allReplaced); + } // // SpawnEntities @@ -754,6 +1011,190 @@ namespace UnitTest m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); } + TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_AllAliasesWithDisabled_NoEntitiesSpawned) + { + using namespace AzFramework; + static constexpr size_t NumEntities = 4; + FillSpawnable(NumEntities); + + InsertEntityAliases( + { 0, 1, 2, 3 }, { 0, 1, 2, 3 }, + { Spawnable::EntityAliasType::Disabled, Spawnable::EntityAliasType::Disabled, Spawnable::EntityAliasType::Disabled, + Spawnable::EntityAliasType::Disabled }); + + 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(0, spawnedEntitiesCount); + } + + TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_SomeAliasesWithDisabled_RegularEntitiesAreSpawned) + { + using namespace AzFramework; + FillSpawnable(8); + InsertEntityAliases<3>( + { 1, 3, 6 }, { 1, 3, 6 }, + { Spawnable::EntityAliasType::Disabled, Spawnable::EntityAliasType::Disabled, Spawnable::EntityAliasType::Disabled }); + + AZStd::vector indices = { 0, 2, 3, 1, 2, 3, 0, 1, 6, 4, 5, 7, 4, 1, 0, 6 }; + + 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(9, spawnedEntitiesCount); + } + + TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_AllAliasesWithReplace_EntitiesSpawnedFromTarget) + { + using namespace AzFramework; + static constexpr size_t NumEntities = 4; + FillSpawnable(NumEntities); + AZ::Data::Asset target = CreateTargetSpawnable(4); + InsertEntityAliases<4>( + { 0, 1, 2, 3 }, { 0, 1, 2, 3 }, + { Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, + Spawnable::EntityAliasType::Replace }, + &target); + + AZStd::vector indices = { 0, 2, 3, 1 }; + + size_t spawnedEntitiesCount = 0; + bool allReplaced = true; + auto callback = [&spawnedEntitiesCount, &allReplaced]( + AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) + { + spawnedEntitiesCount += entities.size(); + for (const AZ::Entity* entity : entities) + { + if (entity) + { + allReplaced = allReplaced && entity->FindComponent() == nullptr; + allReplaced = allReplaced && entity->FindComponent() != nullptr; + } + else + { + allReplaced = false; + } + } + }; + 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(4, spawnedEntitiesCount); + EXPECT_TRUE(allReplaced); + } + + TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_AllAliasesWithAdditional_SourceAndTargetComponentsMerged) + { + using namespace AzFramework; + static constexpr size_t NumEntities = 4; + FillSpawnable(NumEntities); + AZ::Data::Asset target = CreateTargetSpawnable(4); + InsertEntityAliases<4>( + { 0, 1, 2, 3 }, { 0, 1, 2, 3 }, + { Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional, + Spawnable::EntityAliasType::Additional }, + &target); + + AZStd::vector indices = { 0, 2, 3, 1 }; + + size_t spawnedEntitiesCount = 0; + bool allReplaced = true; + auto callback = [&spawnedEntitiesCount, &allReplaced]( + AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) + { + spawnedEntitiesCount += entities.size(); + bool onSource = true; + for (const AZ::Entity* entity : entities) + { + if (entity) + { + if (onSource) + { + allReplaced = allReplaced && entity->FindComponent() != nullptr; + allReplaced = allReplaced && entity->FindComponent() == nullptr; + } + else + { + allReplaced = allReplaced && entity->FindComponent() == nullptr; + allReplaced = allReplaced && entity->FindComponent() != nullptr; + } + onSource = !onSource; + } + else + { + allReplaced = false; + } + } + }; + 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(8, spawnedEntitiesCount); + EXPECT_TRUE(allReplaced); + } + + TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_AllAliasesWithMerge_SourceAndTargetComponentsMerged) + { + using namespace AzFramework; + static constexpr size_t NumEntities = 4; + FillSpawnable(NumEntities); + AZ::Data::Asset target = CreateTargetSpawnable(4); + InsertEntityAliases<4>( + { 0, 1, 2, 3 }, { 0, 1, 2, 3 }, + { Spawnable::EntityAliasType::Merge, Spawnable::EntityAliasType::Merge, Spawnable::EntityAliasType::Merge, + Spawnable::EntityAliasType::Merge }, + &target); + + AZStd::vector indices = { 0, 2, 3, 1 }; + + size_t spawnedEntitiesCount = 0; + bool allReplaced = true; + auto callback = [&spawnedEntitiesCount, &allReplaced]( + AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) + { + spawnedEntitiesCount += entities.size(); + for (const AZ::Entity* entity : entities) + { + if (entity) + { + allReplaced = allReplaced && entity->FindComponent() != nullptr; + allReplaced = allReplaced && entity->FindComponent() != nullptr; + } + else + { + allReplaced = false; + } + } + }; + 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(4, spawnedEntitiesCount); + EXPECT_TRUE(allReplaced); + } // // DespawnAllEntities diff --git a/Code/Framework/AzFramework/Tests/Spawnable/SpawnableTests.cpp b/Code/Framework/AzFramework/Tests/Spawnable/SpawnableTests.cpp new file mode 100644 index 0000000000..f7d6d5190f --- /dev/null +++ b/Code/Framework/AzFramework/Tests/Spawnable/SpawnableTests.cpp @@ -0,0 +1,499 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include + +namespace UnitTest +{ + class SpawnableTest : public AllocatorsFixture + { + public: + void SetUp() override + { + AllocatorsFixture::SetUp(); + + m_spawnable = aznew AzFramework::Spawnable(); + } + + void TearDown() override + { + delete m_spawnable; + m_spawnable = nullptr; + + AllocatorsFixture::TearDown(); + } + + void InsertEightEntities() + { + AzFramework::Spawnable::EntityList& entities = m_spawnable->GetEntities(); + entities.reserve(entities.size() + 8); + for (size_t i = 0; i < 8; ++i) + { + entities.emplace_back(AZStd::make_unique()); + } + } + + void InsertEightEntityAliases( + const AZStd::array& sourceIds, + const AZStd::array& targetIds, + const AZStd::array& aliasTypes, + bool queueLoad = false) + { + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + + for (uint32_t i = 0; i < 8; ++i) + { + AZ::Data::Asset spawnable( + AZ::Data::AssetId(AZ::Uuid("{4CBEC17A-52D6-42D5-9037-F4C05B9CE1D9}"), i), azrtti_typeid()); + visitor.AddAlias(spawnable, AZ::Crc32(i), sourceIds[i], targetIds[i], aliasTypes[i], queueLoad); + } + } + + void InsertEightEntityAliases(bool queueLoad) + { + using namespace AzFramework; + InsertEightEntityAliases( + { 0, 1, 2, 3, 4, 5, 6, 7 }, { 0, 1, 2, 3, 4, 5, 6, 7 }, + { Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, + Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, + Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace }, + queueLoad); + } + + void InsertEightEntityAliases() + { + InsertEightEntityAliases(false); + } + + protected: + AzFramework::Spawnable* m_spawnable; + }; + + + // + // TryGetAliasesConst + // + + TEST_F(SpawnableTest, TryGetAliasesConst_GetVisitor_VisitorDataIsAvailable) + { + AzFramework::Spawnable::EntityAliasConstVisitor visitor = m_spawnable->TryGetAliasesConst(); + EXPECT_TRUE(visitor.IsSet()); + } + + TEST_F(SpawnableTest, TryGetAliasesConst_VisitorThatIsNotReadShared_VisitorDataIsNotAvailable) + { + AzFramework::Spawnable::EntityAliasVisitor readWriteVisitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(readWriteVisitor.IsSet()); + + AzFramework::Spawnable::EntityAliasConstVisitor visitor = m_spawnable->TryGetAliasesConst(); + EXPECT_FALSE(visitor.IsSet()); + } + + TEST_F(SpawnableTest, TryGetAliasesConst_VisitorThatIsAlreadyReadShared_VisitorDataIsAvailable) + { + AzFramework::Spawnable::EntityAliasConstVisitor readVisitor = m_spawnable->TryGetAliasesConst(); + ASSERT_TRUE(readVisitor.IsSet()); + + AzFramework::Spawnable::EntityAliasConstVisitor visitor = m_spawnable->TryGetAliasesConst(); + EXPECT_TRUE(visitor.IsSet()); + } + + + // + // TryGetAliases + // + + TEST_F(SpawnableTest, TryGetAliases_GetVisitor_VisitorDataIsAvailable) + { + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + EXPECT_TRUE(visitor.IsSet()); + } + + TEST_F(SpawnableTest, TryGetAliasesConst_VisitorThatIsAlreadyShared_VisitorDataNotIsAvailable) + { + AzFramework::Spawnable::EntityAliasConstVisitor readVisitor = m_spawnable->TryGetAliasesConst(); + ASSERT_TRUE(readVisitor.IsSet()); + + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + EXPECT_FALSE(visitor.IsSet()); + } + + + // + // EntityAliasVisitor + // + + + // + // HasAliases + // + + TEST_F(SpawnableTest, EntityAliasVisitor_HasAliases_EmptyAliasList_ReturnsFalse) + { + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(visitor.IsSet()); + + EXPECT_FALSE(visitor.HasAliases()); + } + + TEST_F(SpawnableTest, EntityAliasVisitor_HasAliases_FilledInAliasList_ReturnsTue) + { + InsertEightEntities(); + InsertEightEntityAliases(); + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(visitor.IsSet()); + + EXPECT_TRUE(visitor.HasAliases()); + } + + + // + // Optimize + // + + TEST_F(SpawnableTest, EntityAliasVisitor_Optimize_SortEntityAliases_AliasesAreSortedBySourceAndTargetId) + { + InsertEightEntities(); + InsertEightEntityAliases(); + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(visitor.IsSet()); + + // Optimize doesn't need to be explicitly called because the setup of the aliases will cause the alias list to be sorted and optimized. + + uint32_t sourceIndex = 0; + uint32_t targetIndex = 0; + for (const AzFramework::Spawnable::EntityAlias& alias : visitor) + { + if (alias.m_sourceIndex != sourceIndex) + { + ASSERT_LE(sourceIndex, alias.m_sourceIndex); + } + else + { + ASSERT_LE(targetIndex, alias.m_targetIndex); + } + sourceIndex = alias.m_sourceIndex; + targetIndex = alias.m_targetIndex; + } + } + + TEST_F( + SpawnableTest, EntityAliasVisitor_Optimize_RemoveUnused_OnlySecondToLastAliasRemains) + { + using namespace AzFramework; + InsertEightEntities(); + InsertEightEntityAliases( + { 0, 0, 0, 0, 0, 0, 0, 0 }, { 0, 1, 2, 3, 4, 5, 6, 7 }, + { Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Disabled, + Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Disabled, + Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Original }); + + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(visitor.IsSet()); + + EXPECT_EQ(1, AZStd::distance(visitor.begin(), visitor.end())); + EXPECT_EQ(Spawnable::EntityAliasType::Replace, visitor.begin()->m_aliasType); + EXPECT_EQ(6, visitor.begin()->m_targetIndex); + } + + TEST_F(SpawnableTest, EntityAliasVisitor_Optimize_AddAdditional_ThreeAdditionalAliasesAreAdded) + { + using namespace AzFramework; + InsertEightEntities(); + InsertEightEntityAliases( + { 0, 0, 0, 0, 1, 2, 2, 2 }, { 0, 1, 2, 3, 4, 5, 6, 7 }, + { Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional, + Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional, + Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional }); + + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(visitor.IsSet()); + + EXPECT_EQ(11, AZStd::distance(visitor.begin(), visitor.end())); + EXPECT_EQ(Spawnable::EntityAliasType::Original, visitor.begin()->m_aliasType); + EXPECT_EQ(Spawnable::EntityAliasType::Original, visitor.begin()[5].m_aliasType); + EXPECT_EQ(Spawnable::EntityAliasType::Original, visitor.begin()[7].m_aliasType); + } + + TEST_F(SpawnableTest, EntityAliasVisitor_Optimize_OriginalsOnly_AliasListIsEmpty) + { + using namespace AzFramework; + InsertEightEntities(); + InsertEightEntityAliases( + { 0, 0, 0, 0, 0, 0, 0, 0 }, { 0, 1, 2, 3, 4, 5, 6, 7 }, + { Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original, + Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original, + Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original }); + + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(visitor.IsSet()); + + EXPECT_EQ(0, AZStd::distance(visitor.begin(), visitor.end())); + } + + TEST_F(SpawnableTest, EntityAliasVisitor_Optimize_MixedOriginals_AllOriginalsRemoved) + { + using namespace AzFramework; + InsertEightEntities(); + InsertEightEntityAliases( + { 0, 0, 0, 1, 1, 2, 2, 2 }, { 0, 1, 2, 3, 4, 5, 6, 7 }, + { Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original, + Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Disabled, Spawnable::EntityAliasType::Original, + Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Original }); + + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(visitor.IsSet()); + + EXPECT_EQ(2, AZStd::distance(visitor.begin(), visitor.end())); + EXPECT_EQ(Spawnable::EntityAliasType::Disabled, visitor.begin()->m_aliasType); + EXPECT_EQ(Spawnable::EntityAliasType::Replace, visitor.begin()[1].m_aliasType); + } + + TEST_F(SpawnableTest, EntityAliasVisitor_Optimize_MergeAfterOriginal_NoAdditionalOriginalIsInserted) + { + using namespace AzFramework; + InsertEightEntities(); + InsertEightEntityAliases( + { 0, 0, 1, 1, 2, 2, 2, 2 }, { 0, 1, 2, 3, 4, 5, 6, 7 }, + { Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Merge, Spawnable::EntityAliasType::Replace, + Spawnable::EntityAliasType::Merge, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original, + Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original }); + + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(visitor.IsSet()); + + EXPECT_EQ(4, AZStd::distance(visitor.begin(), visitor.end())); + EXPECT_EQ(Spawnable::EntityAliasType::Original, visitor.begin()->m_aliasType); + EXPECT_EQ(Spawnable::EntityAliasType::Merge, visitor.begin()[1].m_aliasType); + EXPECT_EQ(Spawnable::EntityAliasType::Replace, visitor.begin()[2].m_aliasType); + EXPECT_EQ(Spawnable::EntityAliasType::Merge, visitor.begin()[3].m_aliasType); + } + + + // + // UpdateAliasType + // + + TEST_F(SpawnableTest, EntityAliasVisitor_UpdateAliasType_AllToOriginal_NoAliasesAfterOptimization) + { + using namespace AzFramework; + InsertEightEntities(); + InsertEightEntityAliases( + { 0, 1, 2, 3, 4, 5, 6, 7 }, { 0, 1, 2, 3, 4, 5, 6, 7 }, + { Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, + Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, + Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace }); + + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(visitor.IsSet()); + + for (uint32_t i = 0; i < 8; ++i) + { + visitor.UpdateAliasType(i, Spawnable::EntityAliasType::Original); + } + + for (const Spawnable::EntityAlias& alias : visitor) + { + EXPECT_EQ(Spawnable::EntityAliasType::Original, alias.m_aliasType); + } + + visitor.Optimize(); + + EXPECT_EQ(0, AZStd::distance(visitor.begin(), visitor.end())); + } + + + // + // UpdateAliases + // + + TEST_F(SpawnableTest, EntityAliasVisitor_UpdateAliases_AllToOriginal_NoAliasesAfterOptimization) + { + using namespace AzFramework; + InsertEightEntities(); + InsertEightEntityAliases( + { 0, 1, 2, 3, 4, 5, 6, 7 }, { 0, 1, 2, 3, 4, 5, 6, 7 }, + { Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, + Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, + Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace }); + + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(visitor.IsSet()); + + auto callback = + [](Spawnable::EntityAliasType& aliasType, bool& /*queueLoad*/, const AZ::Data::Asset& /*aliasedSpawnable*/, + const AZ::Crc32 /*tag*/, const uint32_t /*sourceIndex*/, const uint32_t /*targetIndex*/) + { + aliasType = Spawnable::EntityAliasType::Original; + }; + visitor.UpdateAliases(AZStd::move(callback)); + + for (const Spawnable::EntityAlias& alias : visitor) + { + EXPECT_EQ(Spawnable::EntityAliasType::Original, alias.m_aliasType); + } + + visitor.Optimize(); + + EXPECT_EQ(0, AZStd::distance(visitor.begin(), visitor.end())); + } + + TEST_F(SpawnableTest, EntityAliasVisitor_UpdateAliases_FilterByTag_OnlyOneAliasUpdated) + { + using namespace AzFramework; + InsertEightEntities(); + InsertEightEntityAliases( + { 0, 1, 2, 3, 4, 5, 6, 7 }, { 0, 1, 2, 3, 4, 5, 6, 7 }, + { Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, + Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, + Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace }); + + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(visitor.IsSet()); + + bool correctTag = false; + size_t numberOfUpdates = 0; + auto callback = [&correctTag, &numberOfUpdates](Spawnable::EntityAliasType& aliasType, bool& /*queueLoad*/, + const AZ::Data::Asset& /*aliasedSpawnable*/, const AZ::Crc32 tag, const uint32_t /*sourceIndex*/, + const uint32_t /*targetIndex*/) + { + correctTag = (tag == AZ::Crc32(3)); + numberOfUpdates++; + aliasType = Spawnable::EntityAliasType::Original; + }; + visitor.UpdateAliases(AZ::Crc32(3), AZStd::move(callback)); + + EXPECT_EQ(Spawnable::EntityAliasType::Original, visitor.begin()[3].m_aliasType); + } + + + // + // AreAllSpawnablesReady + // + + TEST_F(SpawnableTest, EntityAliasVisitor_AreAllSpawnablesReady_CheckFakeLoadedAssets_ReturnsTrue) + { + using namespace AzFramework; + InsertEightEntities(); + InsertEightEntityAliases(); + + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(visitor.IsSet()); + + EXPECT_TRUE(visitor.AreAllSpawnablesReady()); + } + + TEST_F(SpawnableTest, EntityAliasVisitor_AreAllSpawnablesReady_CheckFakeNotLoadedAssets_ReturnsFalse) + { + using namespace AzFramework; + InsertEightEntities(); + InsertEightEntityAliases(true); + + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(visitor.IsSet()); + + EXPECT_FALSE(visitor.AreAllSpawnablesReady()); + } + + + // + // ListTargetSpawnables + // + + TEST_F(SpawnableTest, EntityAliasVisitor_ListTargetSpawnables_ListAllTargetAssets_AllTargetsListed) + { + using namespace AzFramework; + InsertEightEntities(); + InsertEightEntityAliases(); + + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(visitor.IsSet()); + + size_t count = 0; + bool correctAssets = true; + auto callback = [&count, &correctAssets](const AZ::Data::Asset& targetSpawnable) + { + correctAssets = correctAssets && (targetSpawnable.GetId().m_subId == count); + count++; + }; + visitor.ListTargetSpawnables(callback); + + EXPECT_EQ(8, count); + EXPECT_TRUE(correctAssets); + } + + TEST_F(SpawnableTest, EntityAliasVisitor_ListTargetSpawnables_ListTaggedTargetAssets_OneAssetListed) + { + using namespace AzFramework; + InsertEightEntities(); + InsertEightEntityAliases(); + + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(visitor.IsSet()); + + size_t count = 0; + bool correctAsset = false; + auto callback = [&count, &correctAsset](const AZ::Data::Asset& targetSpawnable) + { + correctAsset = (targetSpawnable.GetId().m_subId == 3); + count++; + }; + visitor.ListTargetSpawnables(AZ::Crc32(3), callback); + + EXPECT_EQ(1, count); + EXPECT_TRUE(correctAsset); + } + + + // + // ListSpawnablesRequiringLoad + // + + TEST_F(SpawnableTest, EntityAliasVisitor_ListSpawnablesRequiringLoad_AllSetToLoaded_AllTargetsListed) + { + using namespace AzFramework; + InsertEightEntities(); + InsertEightEntityAliases(true); + + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(visitor.IsSet()); + + size_t count = 0; + bool correctAssets = true; + auto callback = [&count, &correctAssets](const AZ::Data::Asset& targetSpawnable) + { + correctAssets = correctAssets && (targetSpawnable.GetId().m_subId == count); + count++; + }; + visitor.ListSpawnablesRequiringLoad(callback); + + EXPECT_EQ(8, count); + EXPECT_TRUE(correctAssets); + } + + TEST_F(SpawnableTest, EntityAliasVisitor_ListSpawnablesRequiringLoad_AllSetToNotLoaded_NoTargetsListed) + { + using namespace AzFramework; + InsertEightEntities(); + InsertEightEntityAliases(false); + + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(visitor.IsSet()); + + size_t count = 0; + auto callback = [&count](const AZ::Data::Asset& /*targetSpawnable*/) + { + count++; + }; + visitor.ListSpawnablesRequiringLoad(callback); + + EXPECT_EQ(0, count); + } +} // namespace UnitTest diff --git a/Code/Framework/AzFramework/Tests/frameworktests_files.cmake b/Code/Framework/AzFramework/Tests/frameworktests_files.cmake index 6c4f611352..e4877e34a9 100644 --- a/Code/Framework/AzFramework/Tests/frameworktests_files.cmake +++ b/Code/Framework/AzFramework/Tests/frameworktests_files.cmake @@ -10,6 +10,7 @@ set(FILES Main.cpp Spawnable/SpawnableEntitiesInterfaceTests.cpp Spawnable/SpawnableEntitiesManagerTests.cpp + Spawnable/SpawnableTests.cpp ArchiveCompressionTests.cpp ArchiveTests.cpp BehaviorEntityTests.cpp diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.cpp index e12ddb616f..21fa3db52b 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.cpp @@ -220,7 +220,7 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils if (it == aliasVisitors.end()) { AzFramework::Spawnable::EntityAliasVisitor visitor = source->m_spawnable.TryGetAliases(); - AZ_Assert(visitor.HasLock(), "Unable to obtain lock for a newly create spawnable."); + AZ_Assert(visitor.IsSet(), "Unable to obtain lock for a newly create spawnable."); it = aliasVisitors.emplace(source->m_spawnable.GetId(), AZStd::move(visitor)).first; } it->second.AddAlias( diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.h index 7fc01367b2..c937b974e9 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.h @@ -42,7 +42,8 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils { NoLoad, //!< Don't load the spawnable referenced in the entity alias. Loading will be up to the caller. QueueLoad, //!< Queue the spawnable referenced in the entity alias for loading. This will be an async load because asset - //!< handlers aren't allowed to start a blocking load as this can lead to deadlocks. + //!< handlers aren't allowed to start a blocking load as this can lead to deadlocks. This option will allow + //!< to disable loading the referenced spawnable through the event fired from the spawnables asset handler. DependentLoad //!< The spawnable referenced in the entity alias is made a dependency of the spawnable that holds the entity //!< alias. This will cause the spawnable to be automatically loaded along with the owning spawnable. }; From 045a826c681c1ce903fa7924c7f1d781d784d238 Mon Sep 17 00:00:00 2001 From: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> Date: Mon, 1 Nov 2021 14:18:04 -0700 Subject: [PATCH 08/14] Updates to the Spawnable entity aliases based on provided feedback on PR. Signed-off-by: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> --- .../Spawnable/RootSpawnableInterface.h | 4 ++-- .../AzFramework/Spawnable/Spawnable.cpp | 16 +++++----------- .../AzFramework/Spawnable/Spawnable.h | 7 +++++-- .../Spawnable/SpawnableAssetHandler.cpp | 1 + .../Spawnable/SpawnableEntitiesContainer.h | 4 ++-- .../Spawnable/SpawnableEntitiesManager.cpp | 2 +- .../Spawnable/SpawnableEntitiesManagerTests.cpp | 12 ++++++------ .../Tests/Spawnable/SpawnableTests.cpp | 8 ++++---- .../Prefab/Spawnable/PrefabProcessorContext.h | 2 +- .../Prefab/Spawnable/SpawnableUtils.cpp | 6 +++--- 10 files changed, 30 insertions(+), 32 deletions(-) diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/RootSpawnableInterface.h b/Code/Framework/AzFramework/AzFramework/Spawnable/RootSpawnableInterface.h index 873123f38b..d3fe62eae3 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/RootSpawnableInterface.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/RootSpawnableInterface.h @@ -30,7 +30,7 @@ namespace AzFramework //! Called when the root spawnable has been assigned a new value. This may be called several times without a call to release //! in between. - //! NOTE: The callback is not queued but immediately called from a random thread. This is done because this callback is typically + //! @note: The callback is not queued but immediately called from a random thread. This is done because this callback is typically //! used before entities are spawned and if it's queued then the entities spawn before this callback is called. //! @param rootSpawnable The new root spawnable that was assigned. //! @param generation The generation of the root spawnable. This will increment every time a new spawnable is assigned. @@ -38,7 +38,7 @@ namespace AzFramework [[maybe_unused]] uint32_t generation) {} //! Called when the root spawnable has completed spawning of entities. This may be called several times without a call to release //! in between. - //! NOTE: This callback is queued and will be called with a delay and from the main thread. + //! @note: This callback is queued and will be called with a delay and from the main thread. //! @param rootSpawnable The new root spawnable that was used to spawn entities from. //! @param generation The generation of the root spawnable. This will increment every time a new spawnable is assigned. virtual void OnRootSpawnableReady( diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp index 7548a8f181..a3cbf861cb 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp @@ -20,7 +20,6 @@ namespace AzFramework // EntityAlias // - bool Spawnable::EntityAlias::HasLowerIndex(const EntityAlias& other) const { return m_sourceIndex == other.m_sourceIndex ? @@ -51,7 +50,7 @@ namespace AzFramework { if (!alias.m_queueLoad || alias.m_aliasType == Spawnable::EntityAliasType::Original || - alias.m_aliasType == Spawnable::EntityAliasType::Disabled) + alias.m_aliasType == Spawnable::EntityAliasType::Disable) { continue; } @@ -132,7 +131,6 @@ namespace AzFramework // EntityAliasVisitor // - Spawnable::EntityAliasVisitor::EntityAliasVisitor(Spawnable& owner, EntityAliasList* entityAliasList) : m_owner(owner) , m_entityAliasList(entityAliasList) @@ -167,11 +165,7 @@ namespace AzFramework if (this != &rhs) { this->~EntityAliasVisitor(); - *this = EntityAliasVisitor(rhs.m_owner, rhs.m_entityAliasList); - m_dirty = rhs.m_dirty; - - rhs.m_entityAliasList = nullptr; - rhs.m_dirty = false; + new(this) EntityAliasVisitor(AZStd::move(rhs)); } return *this; } @@ -249,7 +243,7 @@ namespace AzFramework { if (alias.m_queueLoad && alias.m_aliasType != Spawnable::EntityAliasType::Original && - alias.m_aliasType != Spawnable::EntityAliasType::Disabled && + alias.m_aliasType != Spawnable::EntityAliasType::Disable && !alias.m_spawnable.IsLoading() && !alias.m_spawnable.IsReady() && !alias.m_spawnable.IsError()) @@ -336,14 +330,14 @@ namespace AzFramework { case Spawnable::EntityAliasType::Original: [[fallthrough]]; - case Spawnable::EntityAliasType::Disabled: + case Spawnable::EntityAliasType::Disable: [[fallthrough]]; case Spawnable::EntityAliasType::Replace: // If the previous entry was a disabled, original or replace alias then remove it as it will be overwritten by the // current entry. if (previousIndex == it->m_sourceIndex && (previousType == Spawnable::EntityAliasType::Original || - previousType == Spawnable::EntityAliasType::Disabled || + previousType == Spawnable::EntityAliasType::Disable || previousType == Spawnable::EntityAliasType::Replace)) { previousIndex = it->m_sourceIndex; diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h index 0a35b81fb1..9058ac7ba4 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h @@ -34,7 +34,7 @@ namespace AzFramework enum class EntityAliasType : uint8_t { Original, //!< The original entity is spawned. - Disabled, //!< No entity will be spawned. + Disable, //!< No entity will be spawned. Replace, //!< The entity alias is spawned instead of the original. Additional, //!< The original entity is spawned as well as the alias. The alias will get a new entity id. Merge //!< The original entity is spawned and the components of the alias are added. The caller is responsible for @@ -105,6 +105,9 @@ namespace AzFramework bool HasAliases() const; bool AreAllSpawnablesReady() const; + // Modification of aliases is limited to specific changes that can only be done through the available modification functions. + // For this reason access through iterators is limited to unmodifiable constant iterators. + EntityAliasList::const_iterator begin() const; EntityAliasList::const_iterator end() const; EntityAliasList::const_iterator cbegin() const; @@ -146,7 +149,7 @@ namespace AzFramework class EntityAliasConstVisitor final : public EntityAliasVisitorBase { public: - EntityAliasConstVisitor(const Spawnable& owner, const EntityAliasList* m_entityAliasList); + EntityAliasConstVisitor(const Spawnable& owner, const EntityAliasList* entityAliasList); ~EntityAliasConstVisitor(); //! Checks if the visitor was able to retrieve data. This needs to be checked before calling any other functions. diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableAssetHandler.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableAssetHandler.cpp index 0cd9891948..6ef423fa91 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableAssetHandler.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableAssetHandler.cpp @@ -118,6 +118,7 @@ namespace AzFramework SpawnableAssetEventsBus::Broadcast( &SpawnableAssetEvents::OnResolveAliases, aliases, spawnable->GetMetaData(), spawnable->GetEntities()); + // The aliases will only be optimized if OnResolveAliases has made any changes. aliases.Optimize(); aliases.ListSpawnablesRequiringLoad( [&assetLoadFilterCB, streamingDeadline, streamingPriority](AZ::Data::Asset& assetPendingLoad) diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesContainer.h b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesContainer.h index 1ec6e6a665..cabef38ff5 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesContainer.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesContainer.h @@ -79,9 +79,9 @@ namespace AzFramework //! other than the calling thread including the main thread. Note that because the alert is queued it can still be called //! after the container has been deleted or can be called for a previously assigned spawnable. In the latter case check //! if the current generation matches the generation provided with the callback. - //! @callback The function called when the alert triggers. This can be called from a different thread than the one that + //! @param callback The function called when the alert triggers. This can be called from a different thread than the one that //! the one that made the call to Alert. - //! @checkSpawnableIsLoaded If true the alert will also block until the spawnable has been loaded. If false then it will + //! @param checkSpawnableIsLoaded If true the alert will also block until the spawnable has been loaded. If false then it will //! be called after all previous calls have completed, but the spawnable may not be loaded at that point. void Alert(AlertCallback callback, CheckIfSpawnableIsLoaded spawnableCheck = CheckIfSpawnableIsLoaded::No); diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp index d7d3f59154..33fe1601af 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp @@ -327,7 +327,7 @@ namespace AzFramework clone = CloneSingleEntity(entityTemplate, templateToCloneMap, serializeContext); AZ_Assert(clone != nullptr, "Failed to clone spawnable entity."); return clone; - case Spawnable::EntityAliasType::Disabled: + case Spawnable::EntityAliasType::Disable: // Do nothing. return nullptr; case Spawnable::EntityAliasType::Replace: diff --git a/Code/Framework/AzFramework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp b/Code/Framework/AzFramework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp index 38847edc07..83c895bc7a 100644 --- a/Code/Framework/AzFramework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp +++ b/Code/Framework/AzFramework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp @@ -485,8 +485,8 @@ namespace UnitTest FillSpawnable(NumEntities); InsertEntityAliases( { 0, 1, 2, 3 }, { 0, 1, 2, 3 }, - { Spawnable::EntityAliasType::Disabled, Spawnable::EntityAliasType::Disabled, Spawnable::EntityAliasType::Disabled, - Spawnable::EntityAliasType::Disabled }); + { Spawnable::EntityAliasType::Disable, Spawnable::EntityAliasType::Disable, Spawnable::EntityAliasType::Disable, + Spawnable::EntityAliasType::Disable }); size_t spawnedEntitiesCount = 0; auto callback = [&spawnedEntitiesCount](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) @@ -506,7 +506,7 @@ namespace UnitTest using namespace AzFramework; static constexpr size_t NumEntities = 8; FillSpawnable(NumEntities); - InsertEntityAliases<2>({ 1, 3 }, { 1, 3 }, { Spawnable::EntityAliasType::Disabled, Spawnable::EntityAliasType::Disabled }); + InsertEntityAliases<2>({ 1, 3 }, { 1, 3 }, { Spawnable::EntityAliasType::Disable, Spawnable::EntityAliasType::Disable }); size_t spawnedEntitiesCount = 0; auto callback = [&spawnedEntitiesCount](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) @@ -1019,8 +1019,8 @@ namespace UnitTest InsertEntityAliases( { 0, 1, 2, 3 }, { 0, 1, 2, 3 }, - { Spawnable::EntityAliasType::Disabled, Spawnable::EntityAliasType::Disabled, Spawnable::EntityAliasType::Disabled, - Spawnable::EntityAliasType::Disabled }); + { Spawnable::EntityAliasType::Disable, Spawnable::EntityAliasType::Disable, Spawnable::EntityAliasType::Disable, + Spawnable::EntityAliasType::Disable }); AZStd::vector indices = { 0, 2, 3, 1 }; @@ -1043,7 +1043,7 @@ namespace UnitTest FillSpawnable(8); InsertEntityAliases<3>( { 1, 3, 6 }, { 1, 3, 6 }, - { Spawnable::EntityAliasType::Disabled, Spawnable::EntityAliasType::Disabled, Spawnable::EntityAliasType::Disabled }); + { Spawnable::EntityAliasType::Disable, Spawnable::EntityAliasType::Disable, Spawnable::EntityAliasType::Disable }); AZStd::vector indices = { 0, 2, 3, 1, 2, 3, 0, 1, 6, 4, 5, 7, 4, 1, 0, 6 }; diff --git a/Code/Framework/AzFramework/Tests/Spawnable/SpawnableTests.cpp b/Code/Framework/AzFramework/Tests/Spawnable/SpawnableTests.cpp index f7d6d5190f..c689295f17 100644 --- a/Code/Framework/AzFramework/Tests/Spawnable/SpawnableTests.cpp +++ b/Code/Framework/AzFramework/Tests/Spawnable/SpawnableTests.cpp @@ -191,8 +191,8 @@ namespace UnitTest InsertEightEntities(); InsertEightEntityAliases( { 0, 0, 0, 0, 0, 0, 0, 0 }, { 0, 1, 2, 3, 4, 5, 6, 7 }, - { Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Disabled, - Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Disabled, + { Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Disable, + Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Disable, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Original }); AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); @@ -245,14 +245,14 @@ namespace UnitTest InsertEightEntityAliases( { 0, 0, 0, 1, 1, 2, 2, 2 }, { 0, 1, 2, 3, 4, 5, 6, 7 }, { Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original, - Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Disabled, Spawnable::EntityAliasType::Original, + Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Disable, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Original }); AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); ASSERT_TRUE(visitor.IsSet()); EXPECT_EQ(2, AZStd::distance(visitor.begin(), visitor.end())); - EXPECT_EQ(Spawnable::EntityAliasType::Disabled, visitor.begin()->m_aliasType); + EXPECT_EQ(Spawnable::EntityAliasType::Disable, visitor.begin()->m_aliasType); EXPECT_EQ(Spawnable::EntityAliasType::Replace, visitor.begin()[1].m_aliasType); } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.h index c937b974e9..8e29deadca 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.h @@ -27,7 +27,7 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils { enum class EntityAliasType : uint8_t { - Disabled, //!< No alias is added. + Disable, //!< No alias is added. OptionalReplace, //!< At runtime the entity might be replaced. If the alias is disabled the original entity will be spawned. //!< The original entity will be left in the spawnable and a copy is returned. Replace, //!< At runtime the entity will be replaced. If the alias is disabled nothing will be spawned not. The original diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.cpp index 154337e957..7b14ad1228 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.cpp @@ -114,9 +114,9 @@ namespace AzToolsFramework::Prefab::SpawnableUtils switch (aliasType) { - case PCU::EntityAliasType::Disabled: + case PCU::EntityAliasType::Disable: // No need to do anything as the alias is disabled. - return ResultPair(nullptr, AzFramework::Spawnable::EntityAliasType::Disabled); + return ResultPair(nullptr, AzFramework::Spawnable::EntityAliasType::Disable); case PCU::EntityAliasType::OptionalReplace: return ResultPair(CloneEntity(entity, source), AzFramework::Spawnable::EntityAliasType::Replace); case PCU::EntityAliasType::Replace: @@ -129,7 +129,7 @@ namespace AzToolsFramework::Prefab::SpawnableUtils default: AZ_Assert( false, "Invalid PrefabProcessorContext::EntityAliasType type (%i) provided.", aznumeric_cast(aliasType)); - return ResultPair(nullptr, AzFramework::Spawnable::EntityAliasType::Disabled); + return ResultPair(nullptr, AzFramework::Spawnable::EntityAliasType::Disable); } } } From 567702931f8d20131507df5fb38c72120345488c Mon Sep 17 00:00:00 2001 From: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> Date: Thu, 4 Nov 2021 19:10:20 -0700 Subject: [PATCH 09/14] Updates for the spawnable entity aliases based on provided feedback. Signed-off-by: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> --- .../AzFramework/Spawnable/Spawnable.cpp | 14 +- .../AzFramework/Spawnable/Spawnable.h | 8 +- .../Spawnable/SpawnableAssetHandler.cpp | 2 +- .../Spawnable/SpawnableEntitiesManager.cpp | 43 ++-- .../Spawnable/SpawnableEntitiesManager.h | 16 +- .../SpawnableEntitiesManagerTests.cpp | 196 ++++++++---------- .../Tests/Spawnable/SpawnableTests.cpp | 164 ++++++++------- .../Spawnable/PrefabProcessorContext.cpp | 2 +- .../Prefab/Spawnable/PrefabProcessorContext.h | 2 +- .../Prefab/Spawnable/SpawnableUtils.cpp | 42 ++-- 10 files changed, 246 insertions(+), 243 deletions(-) diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp index a3cbf861cb..27e8a85597 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp @@ -32,7 +32,7 @@ namespace AzFramework // EntityAliasVisitorBase // - bool Spawnable::EntityAliasVisitorBase::IsSet(const EntityAliasList* aliases) const + bool Spawnable::EntityAliasVisitorBase::IsValid(const EntityAliasList* aliases) const { return aliases != nullptr; } @@ -139,7 +139,7 @@ namespace AzFramework Spawnable::EntityAliasVisitor::~EntityAliasVisitor() { - if (IsSet()) + if (IsValid()) { Optimize(); @@ -170,9 +170,9 @@ namespace AzFramework return *this; } - bool Spawnable::EntityAliasVisitor::IsSet() const + bool Spawnable::EntityAliasVisitor::IsValid() const { - return EntityAliasVisitorBase::IsSet(m_entityAliasList); + return EntityAliasVisitorBase::IsValid(m_entityAliasList); } bool Spawnable::EntityAliasVisitor::HasAliases() const @@ -413,7 +413,7 @@ namespace AzFramework Spawnable::EntityAliasConstVisitor::~EntityAliasConstVisitor() { - if (IsSet()) + if (IsValid()) { AZ_Assert( m_owner.m_shareState <= ShareState::Read, "Attempting to unlock a read shared spawnable that was not in a read shared mode (%i).", @@ -422,9 +422,9 @@ namespace AzFramework } } - bool Spawnable::EntityAliasConstVisitor::IsSet() const + bool Spawnable::EntityAliasConstVisitor::IsValid() const { - return EntityAliasVisitorBase::IsSet(m_entityAliasList); + return EntityAliasVisitorBase::IsValid(m_entityAliasList); } bool Spawnable::EntityAliasConstVisitor::HasAliases() const diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h index 9058ac7ba4..f0aa2c7806 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h @@ -71,7 +71,7 @@ namespace AzFramework class EntityAliasVisitorBase { protected: - bool IsSet(const EntityAliasList* aliases) const; + bool IsValid(const EntityAliasList* aliases) const; bool HasAliases(const EntityAliasList* aliases) const; bool AreAllSpawnablesReady(const EntityAliasList* aliases) const; @@ -100,7 +100,7 @@ namespace AzFramework EntityAliasVisitor& operator=(const EntityAliasVisitor& rhs) = delete; //! Checks if the visitor was able to retrieve data. This needs to be checked before calling any other functions. - bool IsSet() const; + bool IsValid() const; bool HasAliases() const; bool AreAllSpawnablesReady() const; @@ -153,7 +153,7 @@ namespace AzFramework ~EntityAliasConstVisitor(); //! Checks if the visitor was able to retrieve data. This needs to be checked before calling any other functions. - bool IsSet() const; + bool IsValid() const; bool HasAliases() const; bool AreAllSpawnablesReady() const; @@ -179,7 +179,7 @@ namespace AzFramework Spawnable(const Spawnable& rhs) = delete; Spawnable(Spawnable&& other) = delete; ~Spawnable() override = default; - + Spawnable& operator=(const Spawnable& rhs) = delete; Spawnable& operator=(Spawnable&& other) = delete; diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableAssetHandler.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableAssetHandler.cpp index 6ef423fa91..eab681da0d 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableAssetHandler.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableAssetHandler.cpp @@ -103,7 +103,7 @@ namespace AzFramework const AZ::Data::AssetFilterCB& assetLoadFilterCB) { Spawnable::EntityAliasVisitor aliases = spawnable->TryGetAliases(); - AZ_Assert(aliases.IsSet(), "Newly created Spawnable '%s' was already locked.", asset.GetHint().c_str()); + AZ_Assert(aliases.IsValid(), "Newly created Spawnable '%s' was already locked.", asset.GetHint().c_str()); if (aliases.HasAliases()) { AZ_Assert( diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp index 33fe1601af..17a18dd5e5 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp @@ -300,20 +300,20 @@ namespace AzFramework } } - AZ::Entity* SpawnableEntitiesManager::CloneSingleEntity(const AZ::Entity& entityTemplate, - EntityIdMap& templateToCloneMap, AZ::SerializeContext& serializeContext) + AZ::Entity* SpawnableEntitiesManager::CloneSingleEntity(const AZ::Entity& entityPrototype, + EntityIdMap& prototypeToCloneMap, AZ::SerializeContext& serializeContext) { // If the same ID gets remapped more than once, preserve the original remapping instead of overwriting it. constexpr bool allowDuplicateIds = false; return AZ::IdUtils::Remapper::CloneObjectAndGenerateNewIdsAndFixRefs( - &entityTemplate, templateToCloneMap, &serializeContext); + &entityPrototype, prototypeToCloneMap, &serializeContext); } AZ::Entity* SpawnableEntitiesManager::CloneSingleAliasedEntity( - const AZ::Entity& entityTemplate, + const AZ::Entity& entityPrototype, const Spawnable::EntityAlias& alias, - EntityIdMap& templateToCloneMap, + EntityIdMap& prototypeToCloneMap, AZ::Entity* previouslySpawnedEntity, AZ::SerializeContext& serializeContext) { @@ -324,26 +324,27 @@ namespace AzFramework { case Spawnable::EntityAliasType::Original: // Behave as the original version. - clone = CloneSingleEntity(entityTemplate, templateToCloneMap, serializeContext); + clone = CloneSingleEntity(entityPrototype, prototypeToCloneMap, serializeContext); AZ_Assert(clone != nullptr, "Failed to clone spawnable entity."); return clone; case Spawnable::EntityAliasType::Disable: // Do nothing. return nullptr; case Spawnable::EntityAliasType::Replace: - clone = CloneSingleEntity(*(alias.m_spawnable->GetEntities()[alias.m_targetIndex]), templateToCloneMap, serializeContext); + clone = CloneSingleEntity(*(alias.m_spawnable->GetEntities()[alias.m_targetIndex]), prototypeToCloneMap, serializeContext); AZ_Assert(clone != nullptr, "Failed to clone spawnable entity."); return clone; case Spawnable::EntityAliasType::Additional: // The asset handler will have sorted and inserted a Spawnable::EntityAliasType::Original, so the just // spawn the additional entity. - clone = CloneSingleEntity(*(alias.m_spawnable->GetEntities()[alias.m_targetIndex]), templateToCloneMap, serializeContext); + clone = CloneSingleEntity(*(alias.m_spawnable->GetEntities()[alias.m_targetIndex]), prototypeToCloneMap, serializeContext); AZ_Assert(clone != nullptr, "Failed to clone spawnable entity."); return clone; case Spawnable::EntityAliasType::Merge: AZ_Assert(previouslySpawnedEntity != nullptr, "Merging components but there's no entity to add to yet."); AppendComponents( - *previouslySpawnedEntity, alias.m_spawnable->GetEntities()[alias.m_targetIndex]->GetComponents(), templateToCloneMap, serializeContext); + *previouslySpawnedEntity, alias.m_spawnable->GetEntities()[alias.m_targetIndex]->GetComponents(), prototypeToCloneMap, + serializeContext); return nullptr; default: AZ_Assert(false, "Unsupported spawnable entity alias type: %i", alias.m_aliasType); @@ -353,17 +354,17 @@ namespace AzFramework void SpawnableEntitiesManager::AppendComponents( AZ::Entity& target, - const AZ::Entity::ComponentArrayType& componentTemplates, - EntityIdMap& templateToCloneMap, + const AZ::Entity::ComponentArrayType& componentPrototypes, + EntityIdMap& prototypeToCloneMap, AZ::SerializeContext& serializeContext) { // Only components are added and entities are looked up so no duplicate entity ids should be encountered. constexpr bool allowDuplicateIds = false; - for (const AZ::Component* component : componentTemplates) + for (const AZ::Component* component : componentPrototypes) { AZ::Component* clone = AZ::IdUtils::Remapper::CloneObjectAndGenerateNewIdsAndFixRefs( - component, templateToCloneMap, &serializeContext); + component, prototypeToCloneMap, &serializeContext); AZ_Assert(clone, "Unable to clone component for entity '%s' (%zu).", target.GetName().c_str(), target.GetId()); [[maybe_unused]] bool result = target.AddComponent(clone); AZ_Assert(result, "Unable to add cloned component to entity '%s' (%zu).", target.GetName().c_str(), target.GetId()); @@ -409,7 +410,7 @@ namespace AzFramework if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId) { if (Spawnable::EntityAliasConstVisitor aliases = ticket.m_spawnable->TryGetAliasesConst(); - aliases.IsSet() && aliases.AreAllSpawnablesReady()) + aliases.IsValid() && aliases.AreAllSpawnablesReady()) { AZStd::vector& spawnedEntities = ticket.m_spawnedEntities; AZStd::vector& spawnedEntityIndices = ticket.m_spawnedEntityIndices; @@ -417,7 +418,7 @@ namespace AzFramework // Keep track how many entities there were in the array initially size_t spawnedEntitiesInitialCount = spawnedEntities.size(); - // These are 'template' entities we'll be cloning from + // These are 'prototype' entities we'll be cloning from const Spawnable::EntityList& entitiesToSpawn = ticket.m_spawnable->GetEntities(); uint32_t entitiesToSpawnSize = aznumeric_caster(entitiesToSpawn.size()); @@ -527,7 +528,7 @@ namespace AzFramework if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId) { if (Spawnable::EntityAliasConstVisitor aliases = ticket.m_spawnable->TryGetAliasesConst(); - aliases.IsSet() && aliases.AreAllSpawnablesReady()) + aliases.IsValid() && aliases.AreAllSpawnablesReady()) { AZStd::vector& spawnedEntities = ticket.m_spawnedEntities; AZStd::vector& spawnedEntityIndices = ticket.m_spawnedEntityIndices; @@ -538,13 +539,13 @@ namespace AzFramework // 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 + // These are 'prototype' entities we'll be cloning from const Spawnable::EntityList& entitiesToSpawn = ticket.m_spawnable->GetEntities(); size_t entitiesToSpawnSize = request.m_entityIndices.size(); if (ticket.m_entityIdReferenceMap.empty() || !request.m_referencePreviouslySpawnedEntities) { - // This map keeps track of ids from template (spawnable) to clone (instance) allowing patch ups of fields referring + // This map keeps track of ids from prototype (spawnable) to clone (instance) allowing patch ups of fields referring // to entityIds outside of a given entity. // We pre-generate the full set of entity id to new entity id mappings, so that during the clone operation below, // any entity references that point to a not-yet-cloned entity will still get their ids remapped correctly. @@ -754,7 +755,7 @@ namespace AzFramework // Pre-generate the full set of entity id to new entity id mappings, so that during the clone operation below, // any entity references that point to a not-yet-cloned entity will still get their ids remapped correctly. // This map is intentionally cleared out and regenerated here to ensure that we're starting fresh with mappings that - // match the new set of template entities getting spawned. + // match the new set of prototype entities getting spawned. InitializeEntityIdMappings(entities, ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned); if (ticket.m_loadAll) @@ -819,7 +820,7 @@ namespace AzFramework Ticket& ticket = *request.m_ticket; if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId) { - if (Spawnable::EntityAliasVisitor aliases = ticket.m_spawnable->TryGetAliases(); aliases.IsSet()) + if (Spawnable::EntityAliasVisitor aliases = ticket.m_spawnable->TryGetAliases(); aliases.IsValid()) { for (EntityAliasTypeChange& replacement : request.m_entityAliases) { @@ -921,7 +922,7 @@ namespace AzFramework if (request.m_checkAliasSpawnables) { if (Spawnable::EntityAliasConstVisitor visitor = ticket.m_spawnable->TryGetAliasesConst(); - !visitor.IsSet() || !visitor.AreAllSpawnablesReady()) + !visitor.IsValid() || !visitor.AreAllSpawnablesReady()) { return CommandResult::Requeue; } diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h index 09e9b1acfc..d2b5c3c9af 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h @@ -96,9 +96,9 @@ namespace AzFramework AZ_CLASS_ALLOCATOR(Ticket, AZ::ThreadPoolAllocator, 0); static constexpr uint32_t Processing = AZStd::numeric_limits::max(); - //! Map of template entity ids to their associated instance ids. - //! Tickets can be used to spawn the same template entities multiple times, in any order, across multiple calls. - //! Since template entities can reference other entities, this map is used to fix up those references across calls + //! Map of prototype entity ids to their associated instance ids. + //! Tickets can be used to spawn the same prototype entities multiple times, in any order, across multiple calls. + //! Since prototype entities can reference other entities, this map is used to fix up those references across calls //! using the following policy: //! - Entities referencing an entity that hasn't been spawned yet will get a reference to the id that *will* be used //! the first time that entity will be spawned. The reference will be invalid until that entity is spawned, but @@ -243,17 +243,17 @@ namespace AzFramework CommandQueueStatus ProcessQueue(Queue& queue); AZ::Entity* CloneSingleEntity( - const AZ::Entity& entityTemplate, EntityIdMap& templateToCloneMap, AZ::SerializeContext& serializeContext); + const AZ::Entity& entityPrototype, EntityIdMap& prototypeToCloneMap, AZ::SerializeContext& serializeContext); AZ::Entity* CloneSingleAliasedEntity( - const AZ::Entity& entityTemplate, + const AZ::Entity& entityPrototype, const Spawnable::EntityAlias& alias, - EntityIdMap& templateToCloneMap, + EntityIdMap& prototypeToCloneMap, AZ::Entity* previouslySpawnedEntity, AZ::SerializeContext& serializeContext); void AppendComponents( AZ::Entity& target, - const AZ::Entity::ComponentArrayType& componentTemplates, - EntityIdMap& templateToCloneMap, + const AZ::Entity::ComponentArrayType& componentPrototypes, + EntityIdMap& prototypeToCloneMap, AZ::SerializeContext& serializeContext); CommandResult ProcessRequest(SpawnAllEntitiesCommand& request); diff --git a/Code/Framework/AzFramework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp b/Code/Framework/AzFramework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp index 83c895bc7a..535ceab30a 100644 --- a/Code/Framework/AzFramework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp +++ b/Code/Framework/AzFramework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp @@ -192,6 +192,79 @@ namespace UnitTest } } + static bool AreAllEntitiesReplaced(AzFramework::SpawnableConstEntityContainerView entities) + { + for (const AZ::Entity* entity : entities) + { + if (entity) + { + if (entity->FindComponent() != nullptr || + entity->FindComponent() == nullptr) + { + return false; + } + } + else + { + return false; + } + } + return true; + } + + static bool IsEveryOtherEntityAReplacement(AzFramework::SpawnableConstEntityContainerView entities) + { + bool onAlternative = true; + for (const AZ::Entity* entity : entities) + { + if (entity) + { + if (onAlternative) + { + if (entity->FindComponent() == nullptr || + entity->FindComponent() != nullptr) + { + return false; + } + } + else + { + if (entity->FindComponent() != nullptr || + entity->FindComponent() == nullptr) + { + return false; + } + } + onAlternative = !onAlternative; + } + else + { + return false; + } + } + return true; + } + + static bool AreAllMerged(AzFramework::SpawnableConstEntityContainerView entities) + { + for (const AZ::Entity* entity : entities) + { + if (entity) + { + if (entity->FindComponent() == nullptr || + entity->FindComponent() == nullptr) + { + return false; + } + } + else + { + return false; + } + } + return true; + } + void CreateRecursiveHierarchy() { AzFramework::Spawnable::EntityList& entities = m_spawnable->GetEntities(); @@ -539,18 +612,7 @@ namespace UnitTest AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) { spawnedEntitiesCount += entities.size(); - for (const AZ::Entity* entity : entities) - { - if (entity) - { - allReplaced = allReplaced && entity->FindComponent() == nullptr; - allReplaced = allReplaced && entity->FindComponent() != nullptr; - } - else - { - allReplaced = false; - } - } + allReplaced = AreAllEntitiesReplaced(entities); }; AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs; optionalArgs.m_completionCallback = AZStd::move(callback); @@ -574,33 +636,12 @@ namespace UnitTest &target); size_t spawnedEntitiesCount = 0; - bool allReplaced = true; - auto callback = [&spawnedEntitiesCount, &allReplaced]( + bool allAdded = true; + auto callback = [&spawnedEntitiesCount, &allAdded]( AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) { spawnedEntitiesCount += entities.size(); - bool onSource = true; - for (const AZ::Entity* entity : entities) - { - if (entity) - { - if (onSource) - { - allReplaced = allReplaced && entity->FindComponent() != nullptr; - allReplaced = allReplaced && entity->FindComponent() == nullptr; - } - else - { - allReplaced = allReplaced && entity->FindComponent() == nullptr; - allReplaced = allReplaced && entity->FindComponent() != nullptr; - } - onSource = !onSource; - } - else - { - allReplaced = false; - } - } + allAdded = IsEveryOtherEntityAReplacement(entities); }; AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs; optionalArgs.m_completionCallback = AZStd::move(callback); @@ -608,7 +649,7 @@ namespace UnitTest m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); EXPECT_EQ(8, spawnedEntitiesCount); - EXPECT_TRUE(allReplaced); + EXPECT_TRUE(allAdded); } TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_AllAliasesWithMerge_SourceAndTargetComponentsMerged) @@ -624,23 +665,12 @@ namespace UnitTest &target); size_t spawnedEntitiesCount = 0; - bool allReplaced = true; - auto callback = [&spawnedEntitiesCount, &allReplaced]( + bool allMerged = true; + auto callback = [&spawnedEntitiesCount, &allMerged]( AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) { spawnedEntitiesCount += entities.size(); - for (const AZ::Entity* entity : entities) - { - if (entity) - { - allReplaced = allReplaced && entity->FindComponent() != nullptr; - allReplaced = allReplaced && entity->FindComponent() != nullptr; - } - else - { - allReplaced = false; - } - } + allMerged = AreAllMerged(entities); }; AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs; optionalArgs.m_completionCallback = AZStd::move(callback); @@ -648,7 +678,7 @@ namespace UnitTest m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); EXPECT_EQ(4, spawnedEntitiesCount); - EXPECT_TRUE(allReplaced); + EXPECT_TRUE(allMerged); } // @@ -1080,18 +1110,7 @@ namespace UnitTest AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) { spawnedEntitiesCount += entities.size(); - for (const AZ::Entity* entity : entities) - { - if (entity) - { - allReplaced = allReplaced && entity->FindComponent() == nullptr; - allReplaced = allReplaced && entity->FindComponent() != nullptr; - } - else - { - allReplaced = false; - } - } + allReplaced = AreAllEntitiesReplaced(entities); }; AzFramework::SpawnEntitiesOptionalArgs optionalArgs; optionalArgs.m_completionCallback = AZStd::move(callback); @@ -1117,33 +1136,13 @@ namespace UnitTest AZStd::vector indices = { 0, 2, 3, 1 }; size_t spawnedEntitiesCount = 0; - bool allReplaced = true; - auto callback = [&spawnedEntitiesCount, &allReplaced]( + bool allAdded = true; + auto callback = + [&spawnedEntitiesCount, &allAdded]( AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) { spawnedEntitiesCount += entities.size(); - bool onSource = true; - for (const AZ::Entity* entity : entities) - { - if (entity) - { - if (onSource) - { - allReplaced = allReplaced && entity->FindComponent() != nullptr; - allReplaced = allReplaced && entity->FindComponent() == nullptr; - } - else - { - allReplaced = allReplaced && entity->FindComponent() == nullptr; - allReplaced = allReplaced && entity->FindComponent() != nullptr; - } - onSource = !onSource; - } - else - { - allReplaced = false; - } - } + allAdded = IsEveryOtherEntityAReplacement(entities); }; AzFramework::SpawnEntitiesOptionalArgs optionalArgs; optionalArgs.m_completionCallback = AZStd::move(callback); @@ -1151,7 +1150,7 @@ namespace UnitTest m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); EXPECT_EQ(8, spawnedEntitiesCount); - EXPECT_TRUE(allReplaced); + EXPECT_TRUE(allAdded); } TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_AllAliasesWithMerge_SourceAndTargetComponentsMerged) @@ -1169,23 +1168,12 @@ namespace UnitTest AZStd::vector indices = { 0, 2, 3, 1 }; size_t spawnedEntitiesCount = 0; - bool allReplaced = true; - auto callback = [&spawnedEntitiesCount, &allReplaced]( + bool allMerged = true; + auto callback = [&spawnedEntitiesCount, &allMerged]( AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) { spawnedEntitiesCount += entities.size(); - for (const AZ::Entity* entity : entities) - { - if (entity) - { - allReplaced = allReplaced && entity->FindComponent() != nullptr; - allReplaced = allReplaced && entity->FindComponent() != nullptr; - } - else - { - allReplaced = false; - } - } + allMerged = AreAllMerged(entities); }; AzFramework::SpawnEntitiesOptionalArgs optionalArgs; optionalArgs.m_completionCallback = AZStd::move(callback); @@ -1193,7 +1181,7 @@ namespace UnitTest m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); EXPECT_EQ(4, spawnedEntitiesCount); - EXPECT_TRUE(allReplaced); + EXPECT_TRUE(allMerged); } // diff --git a/Code/Framework/AzFramework/Tests/Spawnable/SpawnableTests.cpp b/Code/Framework/AzFramework/Tests/Spawnable/SpawnableTests.cpp index c689295f17..94641037f0 100644 --- a/Code/Framework/AzFramework/Tests/Spawnable/SpawnableTests.cpp +++ b/Code/Framework/AzFramework/Tests/Spawnable/SpawnableTests.cpp @@ -6,6 +6,7 @@ * */ +#include #include #include #include @@ -15,6 +16,8 @@ namespace UnitTest class SpawnableTest : public AllocatorsFixture { public: + static constexpr size_t DefaultEntityAliasTestCount = 8; + void SetUp() override { AllocatorsFixture::SetUp(); @@ -30,25 +33,26 @@ namespace UnitTest AllocatorsFixture::TearDown(); } - void InsertEightEntities() + void InsertEntities(size_t count) { AzFramework::Spawnable::EntityList& entities = m_spawnable->GetEntities(); - entities.reserve(entities.size() + 8); - for (size_t i = 0; i < 8; ++i) + entities.reserve(entities.size() + count); + for (size_t i = 0; i < count; ++i) { entities.emplace_back(AZStd::make_unique()); } } - void InsertEightEntityAliases( - const AZStd::array& sourceIds, - const AZStd::array& targetIds, - const AZStd::array& aliasTypes, + template + void InsertEntityAliases( + const AZStd::array& sourceIds, + const AZStd::array& targetIds, + const AZStd::array& aliasTypes, bool queueLoad = false) { AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); - for (uint32_t i = 0; i < 8; ++i) + for (uint32_t i = 0; i < Count; ++i) { AZ::Data::Asset spawnable( AZ::Data::AssetId(AZ::Uuid("{4CBEC17A-52D6-42D5-9037-F4C05B9CE1D9}"), i), azrtti_typeid()); @@ -56,20 +60,30 @@ namespace UnitTest } } - void InsertEightEntityAliases(bool queueLoad) + template + void InsertEntityAliases(bool queueLoad) { using namespace AzFramework; - InsertEightEntityAliases( - { 0, 1, 2, 3, 4, 5, 6, 7 }, { 0, 1, 2, 3, 4, 5, 6, 7 }, - { Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, - Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, - Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace }, - queueLoad); + + AZStd::array ids; + for (uint32_t i=0; i(Count); ++i) + { + ids[i] = i; + } + + AZStd::array aliasTypes; + for (uint32_t i = 0; i < aznumeric_cast(Count); ++i) + { + aliasTypes[i] = Spawnable::EntityAliasType::Replace; + } + + InsertEntityAliases(ids, ids, aliasTypes, queueLoad); } - void InsertEightEntityAliases() + template + void InsertEntityAliases() { - InsertEightEntityAliases(false); + InsertEntityAliases(false); } protected: @@ -84,25 +98,25 @@ namespace UnitTest TEST_F(SpawnableTest, TryGetAliasesConst_GetVisitor_VisitorDataIsAvailable) { AzFramework::Spawnable::EntityAliasConstVisitor visitor = m_spawnable->TryGetAliasesConst(); - EXPECT_TRUE(visitor.IsSet()); + EXPECT_TRUE(visitor.IsValid()); } TEST_F(SpawnableTest, TryGetAliasesConst_VisitorThatIsNotReadShared_VisitorDataIsNotAvailable) { AzFramework::Spawnable::EntityAliasVisitor readWriteVisitor = m_spawnable->TryGetAliases(); - ASSERT_TRUE(readWriteVisitor.IsSet()); + ASSERT_TRUE(readWriteVisitor.IsValid()); AzFramework::Spawnable::EntityAliasConstVisitor visitor = m_spawnable->TryGetAliasesConst(); - EXPECT_FALSE(visitor.IsSet()); + EXPECT_FALSE(visitor.IsValid()); } TEST_F(SpawnableTest, TryGetAliasesConst_VisitorThatIsAlreadyReadShared_VisitorDataIsAvailable) { AzFramework::Spawnable::EntityAliasConstVisitor readVisitor = m_spawnable->TryGetAliasesConst(); - ASSERT_TRUE(readVisitor.IsSet()); + ASSERT_TRUE(readVisitor.IsValid()); AzFramework::Spawnable::EntityAliasConstVisitor visitor = m_spawnable->TryGetAliasesConst(); - EXPECT_TRUE(visitor.IsSet()); + EXPECT_TRUE(visitor.IsValid()); } @@ -113,16 +127,16 @@ namespace UnitTest TEST_F(SpawnableTest, TryGetAliases_GetVisitor_VisitorDataIsAvailable) { AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); - EXPECT_TRUE(visitor.IsSet()); + EXPECT_TRUE(visitor.IsValid()); } TEST_F(SpawnableTest, TryGetAliasesConst_VisitorThatIsAlreadyShared_VisitorDataNotIsAvailable) { AzFramework::Spawnable::EntityAliasConstVisitor readVisitor = m_spawnable->TryGetAliasesConst(); - ASSERT_TRUE(readVisitor.IsSet()); + ASSERT_TRUE(readVisitor.IsValid()); AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); - EXPECT_FALSE(visitor.IsSet()); + EXPECT_FALSE(visitor.IsValid()); } @@ -138,17 +152,17 @@ namespace UnitTest TEST_F(SpawnableTest, EntityAliasVisitor_HasAliases_EmptyAliasList_ReturnsFalse) { AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); - ASSERT_TRUE(visitor.IsSet()); + ASSERT_TRUE(visitor.IsValid()); EXPECT_FALSE(visitor.HasAliases()); } - TEST_F(SpawnableTest, EntityAliasVisitor_HasAliases_FilledInAliasList_ReturnsTue) + TEST_F(SpawnableTest, EntityAliasVisitor_HasAliases_FilledInAliasList_ReturnsTrue) { - InsertEightEntities(); - InsertEightEntityAliases(); + InsertEntities(8); + InsertEntityAliases<8>(); AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); - ASSERT_TRUE(visitor.IsSet()); + ASSERT_TRUE(visitor.IsValid()); EXPECT_TRUE(visitor.HasAliases()); } @@ -160,10 +174,10 @@ namespace UnitTest TEST_F(SpawnableTest, EntityAliasVisitor_Optimize_SortEntityAliases_AliasesAreSortedBySourceAndTargetId) { - InsertEightEntities(); - InsertEightEntityAliases(); + InsertEntities(DefaultEntityAliasTestCount); + InsertEntityAliases(); AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); - ASSERT_TRUE(visitor.IsSet()); + ASSERT_TRUE(visitor.IsValid()); // Optimize doesn't need to be explicitly called because the setup of the aliases will cause the alias list to be sorted and optimized. @@ -188,15 +202,15 @@ namespace UnitTest SpawnableTest, EntityAliasVisitor_Optimize_RemoveUnused_OnlySecondToLastAliasRemains) { using namespace AzFramework; - InsertEightEntities(); - InsertEightEntityAliases( + InsertEntities(8); + InsertEntityAliases<8>( { 0, 0, 0, 0, 0, 0, 0, 0 }, { 0, 1, 2, 3, 4, 5, 6, 7 }, { Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Disable, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Disable, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Original }); AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); - ASSERT_TRUE(visitor.IsSet()); + ASSERT_TRUE(visitor.IsValid()); EXPECT_EQ(1, AZStd::distance(visitor.begin(), visitor.end())); EXPECT_EQ(Spawnable::EntityAliasType::Replace, visitor.begin()->m_aliasType); @@ -206,15 +220,15 @@ namespace UnitTest TEST_F(SpawnableTest, EntityAliasVisitor_Optimize_AddAdditional_ThreeAdditionalAliasesAreAdded) { using namespace AzFramework; - InsertEightEntities(); - InsertEightEntityAliases( + InsertEntities(8); + InsertEntityAliases<8>( { 0, 0, 0, 0, 1, 2, 2, 2 }, { 0, 1, 2, 3, 4, 5, 6, 7 }, { Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional }); AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); - ASSERT_TRUE(visitor.IsSet()); + ASSERT_TRUE(visitor.IsValid()); EXPECT_EQ(11, AZStd::distance(visitor.begin(), visitor.end())); EXPECT_EQ(Spawnable::EntityAliasType::Original, visitor.begin()->m_aliasType); @@ -225,15 +239,15 @@ namespace UnitTest TEST_F(SpawnableTest, EntityAliasVisitor_Optimize_OriginalsOnly_AliasListIsEmpty) { using namespace AzFramework; - InsertEightEntities(); - InsertEightEntityAliases( + InsertEntities(8); + InsertEntityAliases<8>( { 0, 0, 0, 0, 0, 0, 0, 0 }, { 0, 1, 2, 3, 4, 5, 6, 7 }, { Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original }); AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); - ASSERT_TRUE(visitor.IsSet()); + ASSERT_TRUE(visitor.IsValid()); EXPECT_EQ(0, AZStd::distance(visitor.begin(), visitor.end())); } @@ -241,15 +255,15 @@ namespace UnitTest TEST_F(SpawnableTest, EntityAliasVisitor_Optimize_MixedOriginals_AllOriginalsRemoved) { using namespace AzFramework; - InsertEightEntities(); - InsertEightEntityAliases( + InsertEntities(8); + InsertEntityAliases<8>( { 0, 0, 0, 1, 1, 2, 2, 2 }, { 0, 1, 2, 3, 4, 5, 6, 7 }, { Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Disable, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Original }); AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); - ASSERT_TRUE(visitor.IsSet()); + ASSERT_TRUE(visitor.IsValid()); EXPECT_EQ(2, AZStd::distance(visitor.begin(), visitor.end())); EXPECT_EQ(Spawnable::EntityAliasType::Disable, visitor.begin()->m_aliasType); @@ -259,15 +273,15 @@ namespace UnitTest TEST_F(SpawnableTest, EntityAliasVisitor_Optimize_MergeAfterOriginal_NoAdditionalOriginalIsInserted) { using namespace AzFramework; - InsertEightEntities(); - InsertEightEntityAliases( + InsertEntities(8); + InsertEntityAliases<8>( { 0, 0, 1, 1, 2, 2, 2, 2 }, { 0, 1, 2, 3, 4, 5, 6, 7 }, { Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Merge, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Merge, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original }); AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); - ASSERT_TRUE(visitor.IsSet()); + ASSERT_TRUE(visitor.IsValid()); EXPECT_EQ(4, AZStd::distance(visitor.begin(), visitor.end())); EXPECT_EQ(Spawnable::EntityAliasType::Original, visitor.begin()->m_aliasType); @@ -284,15 +298,15 @@ namespace UnitTest TEST_F(SpawnableTest, EntityAliasVisitor_UpdateAliasType_AllToOriginal_NoAliasesAfterOptimization) { using namespace AzFramework; - InsertEightEntities(); - InsertEightEntityAliases( + InsertEntities(8); + InsertEntityAliases<8>( { 0, 1, 2, 3, 4, 5, 6, 7 }, { 0, 1, 2, 3, 4, 5, 6, 7 }, { Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace }); AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); - ASSERT_TRUE(visitor.IsSet()); + ASSERT_TRUE(visitor.IsValid()); for (uint32_t i = 0; i < 8; ++i) { @@ -317,15 +331,15 @@ namespace UnitTest TEST_F(SpawnableTest, EntityAliasVisitor_UpdateAliases_AllToOriginal_NoAliasesAfterOptimization) { using namespace AzFramework; - InsertEightEntities(); - InsertEightEntityAliases( + InsertEntities(8); + InsertEntityAliases<8>( { 0, 1, 2, 3, 4, 5, 6, 7 }, { 0, 1, 2, 3, 4, 5, 6, 7 }, { Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace }); AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); - ASSERT_TRUE(visitor.IsSet()); + ASSERT_TRUE(visitor.IsValid()); auto callback = [](Spawnable::EntityAliasType& aliasType, bool& /*queueLoad*/, const AZ::Data::Asset& /*aliasedSpawnable*/, @@ -348,15 +362,15 @@ namespace UnitTest TEST_F(SpawnableTest, EntityAliasVisitor_UpdateAliases_FilterByTag_OnlyOneAliasUpdated) { using namespace AzFramework; - InsertEightEntities(); - InsertEightEntityAliases( + InsertEntities(8); + InsertEntityAliases<8>( { 0, 1, 2, 3, 4, 5, 6, 7 }, { 0, 1, 2, 3, 4, 5, 6, 7 }, { Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace }); AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); - ASSERT_TRUE(visitor.IsSet()); + ASSERT_TRUE(visitor.IsValid()); bool correctTag = false; size_t numberOfUpdates = 0; @@ -381,11 +395,11 @@ namespace UnitTest TEST_F(SpawnableTest, EntityAliasVisitor_AreAllSpawnablesReady_CheckFakeLoadedAssets_ReturnsTrue) { using namespace AzFramework; - InsertEightEntities(); - InsertEightEntityAliases(); + InsertEntities(DefaultEntityAliasTestCount); + InsertEntityAliases(); AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); - ASSERT_TRUE(visitor.IsSet()); + ASSERT_TRUE(visitor.IsValid()); EXPECT_TRUE(visitor.AreAllSpawnablesReady()); } @@ -393,11 +407,11 @@ namespace UnitTest TEST_F(SpawnableTest, EntityAliasVisitor_AreAllSpawnablesReady_CheckFakeNotLoadedAssets_ReturnsFalse) { using namespace AzFramework; - InsertEightEntities(); - InsertEightEntityAliases(true); + InsertEntities(8); + InsertEntityAliases<8>(true); AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); - ASSERT_TRUE(visitor.IsSet()); + ASSERT_TRUE(visitor.IsValid()); EXPECT_FALSE(visitor.AreAllSpawnablesReady()); } @@ -410,11 +424,11 @@ namespace UnitTest TEST_F(SpawnableTest, EntityAliasVisitor_ListTargetSpawnables_ListAllTargetAssets_AllTargetsListed) { using namespace AzFramework; - InsertEightEntities(); - InsertEightEntityAliases(); + InsertEntities(DefaultEntityAliasTestCount); + InsertEntityAliases(); AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); - ASSERT_TRUE(visitor.IsSet()); + ASSERT_TRUE(visitor.IsValid()); size_t count = 0; bool correctAssets = true; @@ -432,11 +446,11 @@ namespace UnitTest TEST_F(SpawnableTest, EntityAliasVisitor_ListTargetSpawnables_ListTaggedTargetAssets_OneAssetListed) { using namespace AzFramework; - InsertEightEntities(); - InsertEightEntityAliases(); + InsertEntities(DefaultEntityAliasTestCount); + InsertEntityAliases(); AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); - ASSERT_TRUE(visitor.IsSet()); + ASSERT_TRUE(visitor.IsValid()); size_t count = 0; bool correctAsset = false; @@ -459,11 +473,11 @@ namespace UnitTest TEST_F(SpawnableTest, EntityAliasVisitor_ListSpawnablesRequiringLoad_AllSetToLoaded_AllTargetsListed) { using namespace AzFramework; - InsertEightEntities(); - InsertEightEntityAliases(true); + InsertEntities(DefaultEntityAliasTestCount); + InsertEntityAliases(true); AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); - ASSERT_TRUE(visitor.IsSet()); + ASSERT_TRUE(visitor.IsValid()); size_t count = 0; bool correctAssets = true; @@ -481,11 +495,11 @@ namespace UnitTest TEST_F(SpawnableTest, EntityAliasVisitor_ListSpawnablesRequiringLoad_AllSetToNotLoaded_NoTargetsListed) { using namespace AzFramework; - InsertEightEntities(); - InsertEightEntityAliases(false); + InsertEntities(DefaultEntityAliasTestCount); + InsertEntityAliases(false); AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); - ASSERT_TRUE(visitor.IsSet()); + ASSERT_TRUE(visitor.IsValid()); size_t count = 0; auto callback = [&count](const AZ::Data::Asset& /*targetSpawnable*/) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.cpp index 21fa3db52b..efef0f53de 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.cpp @@ -220,7 +220,7 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils if (it == aliasVisitors.end()) { AzFramework::Spawnable::EntityAliasVisitor visitor = source->m_spawnable.TryGetAliases(); - AZ_Assert(visitor.IsSet(), "Unable to obtain lock for a newly create spawnable."); + AZ_Assert(visitor.IsValid(), "Unable to obtain lock for a newly create spawnable."); it = aliasVisitors.emplace(source->m_spawnable.GetId(), AZStd::move(visitor)).first; } it->second.AddAlias( diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.h index 8e29deadca..f20c85eb1d 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.h @@ -30,7 +30,7 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils Disable, //!< No alias is added. OptionalReplace, //!< At runtime the entity might be replaced. If the alias is disabled the original entity will be spawned. //!< The original entity will be left in the spawnable and a copy is returned. - Replace, //!< At runtime the entity will be replaced. If the alias is disabled nothing will be spawned not. The original + Replace, //!< At runtime the entity will be replaced. If the alias is disabled nothing will be spawned. The original //!< entity is returned and a blank entity is left. Additional, //!< At runtime the alias entity will be added as an additional but unrelated entity with a new entity id. //!< An empty entity will be returned. diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.cpp index 7b14ad1228..dfe82167c2 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.cpp @@ -32,38 +32,38 @@ namespace AzToolsFramework::Prefab::SpawnableUtils return result; } - AZ::Entity* FindEntity(AZ::EntityId entity, AzToolsFramework::Prefab::Instance& source) + AZ::Entity* FindEntity(AZ::EntityId entityId, AzToolsFramework::Prefab::Instance& source) { AZ::Entity* result = nullptr; source.GetEntities( - [&result, entity](AZStd::unique_ptr& instance) + [&result, entityId](AZStd::unique_ptr& entity) { - if (instance->GetId() != entity) + if (entity->GetId() != entityId) { return true; } else { - result = instance.get(); + result = entity.get(); return false; } }); return result; } - AZ::Entity* FindEntity(AZ::EntityId entity, AzFramework::Spawnable& source) + AZ::Entity* FindEntity(AZ::EntityId entityId, AzFramework::Spawnable& source) { - uint32_t index = AzToolsFramework::Prefab::SpawnableUtils::FindEntityIndex(entity, source); + uint32_t index = AzToolsFramework::Prefab::SpawnableUtils::FindEntityIndex(entityId, source); return index != InvalidEntityIndex ? source.GetEntities()[index].get() : nullptr; } template - AZStd::unique_ptr CloneEntity(AZ::EntityId entity, T& source) + AZStd::unique_ptr CloneEntity(AZ::EntityId entityId, T& source) { - AZ::Entity* target = Internal::FindEntity(entity, source); + AZ::Entity* target = Internal::FindEntity(entityId, source); AZ_Assert( target, "SpawnbleUtils were unable to locate entity with id %zu in Instance or Spawnable for cloning.", - aznumeric_cast(entity)); + aznumeric_cast(entityId)); auto clone = AZStd::make_unique(); static AZ::SerializeContext* sc = GetSerializeContext(); @@ -73,12 +73,12 @@ namespace AzToolsFramework::Prefab::SpawnableUtils return clone; } - AZStd::unique_ptr ReplaceEntityWithPlaceholder(AZ::EntityId entity, AzToolsFramework::Prefab::Instance& source) + AZStd::unique_ptr ReplaceEntityWithPlaceholder(AZ::EntityId entityId, AzToolsFramework::Prefab::Instance& source) { - auto&& [instance, alias] = source.FindInstanceAndAlias(entity); + auto&& [instance, alias] = source.FindInstanceAndAlias(entityId); AZ_Assert( instance, "SpawnbleUtils were unable to locate entity alias with id %zu in Instance '%s' for replacing.", - aznumeric_cast(entity), source.GetTemplateSourcePath().c_str()); + aznumeric_cast(entityId), source.GetTemplateSourcePath().c_str()); EntityOptionalReference entityData = instance->GetEntity(alias); AZ_Assert( @@ -88,17 +88,17 @@ namespace AzToolsFramework::Prefab::SpawnableUtils return instance->ReplaceEntity(AZStd::move(placeholder), alias); } - AZStd::unique_ptr ReplaceEntityWithPlaceholder(AZ::EntityId entity, AzFramework::Spawnable& source) + AZStd::unique_ptr ReplaceEntityWithPlaceholder(AZ::EntityId entityId, AzFramework::Spawnable& source) { - uint32_t index = AzToolsFramework::Prefab::SpawnableUtils::FindEntityIndex(entity, source); + uint32_t index = AzToolsFramework::Prefab::SpawnableUtils::FindEntityIndex(entityId, source); AZ_Assert( index != InvalidEntityIndex, "SpawnbleUtils were unable to locate entity alias with id %zu in Spawnable for replacing.", - aznumeric_cast(entity)); + aznumeric_cast(entityId)); AZStd::unique_ptr original = AZStd::move(source.GetEntities()[index]); AZ_Assert( original, "SpawnbleUtils were unable to locate entity with id %zu in Spawnable for replacing.", - aznumeric_cast(entity)); + aznumeric_cast(entityId)); source.GetEntities()[index] = AZStd::make_unique(original->GetId(), original->GetName()); @@ -107,7 +107,7 @@ namespace AzToolsFramework::Prefab::SpawnableUtils template AZStd::pair, AzFramework::Spawnable::EntityAliasType> ApplyAlias( - Source& source, AZ::EntityId entity, AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasType aliasType) + Source& source, AZ::EntityId entityId, AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasType aliasType) { namespace PCU = AzToolsFramework::Prefab::PrefabConversionUtils; using ResultPair = AZStd::pair, AzFramework::Spawnable::EntityAliasType>; @@ -118,14 +118,14 @@ namespace AzToolsFramework::Prefab::SpawnableUtils // No need to do anything as the alias is disabled. return ResultPair(nullptr, AzFramework::Spawnable::EntityAliasType::Disable); case PCU::EntityAliasType::OptionalReplace: - return ResultPair(CloneEntity(entity, source), AzFramework::Spawnable::EntityAliasType::Replace); + return ResultPair(CloneEntity(entityId, source), AzFramework::Spawnable::EntityAliasType::Replace); case PCU::EntityAliasType::Replace: - return ResultPair(ReplaceEntityWithPlaceholder(entity, source), AzFramework::Spawnable::EntityAliasType::Replace); + return ResultPair(ReplaceEntityWithPlaceholder(entityId, source), AzFramework::Spawnable::EntityAliasType::Replace); case PCU::EntityAliasType::Additional: ResultPair(AZStd::make_unique(AZ::Entity::MakeId()), AzFramework::Spawnable::EntityAliasType::Additional); case PCU::EntityAliasType::Merge: // Use the same entity id as the original entity so at runtime the entity ids can be verified to match. - ResultPair(AZStd::make_unique(entity), AzFramework::Spawnable::EntityAliasType::Merge); + ResultPair(AZStd::make_unique(entityId), AzFramework::Spawnable::EntityAliasType::Merge); default: AZ_Assert( false, "Invalid PrefabProcessorContext::EntityAliasType type (%i) provided.", aznumeric_cast(aliasType)); @@ -236,7 +236,7 @@ namespace AzToolsFramework::Prefab::SpawnableUtils } else { - AZ_Assert(false, "Entity with id %zu was not found in the source prefab.", static_cast(entity)); + AZ_Assert(false, "Entity with id %llu was not found in the source prefab.", static_cast(entity)); return nullptr; } } From c9f9a83c57af7c464e6e7d2b3fef1183aac1616e Mon Sep 17 00:00:00 2001 From: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> Date: Mon, 8 Nov 2021 12:00:15 -0800 Subject: [PATCH 10/14] Further PR feedback on the Spawnble Entity Aliases. Signed-off-by: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> --- .../Spawnable/SpawnableEntitiesManager.cpp | 2 +- .../SpawnableEntitiesManagerTests.cpp | 12 +- .../Prefab/Spawnable/SpawnableUtils.cpp | 118 +++++++++++------- .../Prefab/Spawnable/SpawnableUtils.h | 6 +- 4 files changed, 80 insertions(+), 58 deletions(-) diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp index 17a18dd5e5..1e863877a4 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp @@ -944,7 +944,7 @@ namespace AzFramework { for (AZ::Entity* entity : request.m_ticket->m_spawnedEntities) { - if (entity != nullptr && !entity->GetComponents().empty()) + if (entity != nullptr) { // Setting it to 0 is needed to avoid the infinite loop between GameEntityContext and SpawnableEntitiesManager. entity->SetSpawnTicketId(0); diff --git a/Code/Framework/AzFramework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp b/Code/Framework/AzFramework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp index 535ceab30a..0dc00f81dd 100644 --- a/Code/Framework/AzFramework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp +++ b/Code/Framework/AzFramework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp @@ -607,7 +607,7 @@ namespace UnitTest &target); size_t spawnedEntitiesCount = 0; - bool allReplaced = true; + bool allReplaced = false; auto callback = [&spawnedEntitiesCount, &allReplaced]( AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) { @@ -636,7 +636,7 @@ namespace UnitTest &target); size_t spawnedEntitiesCount = 0; - bool allAdded = true; + bool allAdded = false; auto callback = [&spawnedEntitiesCount, &allAdded]( AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) { @@ -665,7 +665,7 @@ namespace UnitTest &target); size_t spawnedEntitiesCount = 0; - bool allMerged = true; + bool allMerged = false; auto callback = [&spawnedEntitiesCount, &allMerged]( AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) { @@ -1105,7 +1105,7 @@ namespace UnitTest AZStd::vector indices = { 0, 2, 3, 1 }; size_t spawnedEntitiesCount = 0; - bool allReplaced = true; + bool allReplaced = false; auto callback = [&spawnedEntitiesCount, &allReplaced]( AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) { @@ -1136,7 +1136,7 @@ namespace UnitTest AZStd::vector indices = { 0, 2, 3, 1 }; size_t spawnedEntitiesCount = 0; - bool allAdded = true; + bool allAdded = false; auto callback = [&spawnedEntitiesCount, &allAdded]( AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) @@ -1168,7 +1168,7 @@ namespace UnitTest AZStd::vector indices = { 0, 2, 3, 1 }; size_t spawnedEntitiesCount = 0; - bool allMerged = true; + bool allMerged = false; auto callback = [&spawnedEntitiesCount, &allMerged]( AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) { diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.cpp index dfe82167c2..ab3c52f6d0 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.cpp @@ -170,7 +170,7 @@ namespace AzToolsFramework::Prefab::SpawnableUtils AzToolsFramework::Prefab::Instance& source, AZStd::string targetPrefabName, AzToolsFramework::Prefab::Instance& target, - AZ::EntityId entity, + AZ::EntityId entityId, AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasType aliasType, AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasSpawnableLoadBehavior loadBehavior, uint32_t tag, @@ -178,28 +178,35 @@ namespace AzToolsFramework::Prefab::SpawnableUtils { using namespace AzToolsFramework::Prefab::PrefabConversionUtils; - AliasPath alias = source.GetAliasPathRelativeToInstance(entity); + AliasPath alias = source.GetAliasPathRelativeToInstance(entityId); if (!alias.empty()) { - auto&& [replacement, storedAliasType] = Internal::ApplyAlias(source, entity, aliasType); + auto&& [replacement, storedAliasType] = Internal::ApplyAlias(source, entityId, aliasType); + if (replacement) + { + AZ::Entity* result = replacement.get(); + target.AddEntity(AZStd::move(replacement), alias.Filename().Native()); - AZ::Entity* result = replacement.get(); - target.AddEntity(AZStd::move(replacement), alias.Filename().Native()); + EntityAliasStore store; + store.m_aliasType = storedAliasType; + store.m_source.emplace(AZStd::move(sourcePrefabName), AZStd::move(alias)); + store.m_target.emplace( + AZStd::move(targetPrefabName), target.GetAliasPathRelativeToInstance(result->GetId())); + store.m_loadBehavior = loadBehavior; + store.m_tag = tag; + context.RegisterSpawnableEntityAlias(AZStd::move(store)); - EntityAliasStore store; - store.m_aliasType = storedAliasType; - store.m_source.emplace(AZStd::move(sourcePrefabName), AZStd::move(alias)); - store.m_target.emplace( - AZStd::move(targetPrefabName), target.GetAliasPathRelativeToInstance(result->GetId())); - store.m_loadBehavior = loadBehavior; - store.m_tag = tag; - context.RegisterSpawnableEntityAlias(AZStd::move(store)); - - return result; + return result; + } + else + { + AZ_Assert(false, "A replacement for entity with id %zu could not be created.", static_cast(entityId)); + return nullptr; + } } else { - AZ_Assert(false, "Entity with id %zu was not found in the source prefab.", static_cast(entity)); + AZ_Assert(false, "Entity with id %zu was not found in the source prefab.", static_cast(entityId)); return nullptr; } } @@ -208,7 +215,7 @@ namespace AzToolsFramework::Prefab::SpawnableUtils AZStd::string sourcePrefabName, AzToolsFramework::Prefab::Instance& source, AzFramework::Spawnable& target, - AZ::EntityId entity, + AZ::EntityId entityId, AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasType aliasType, AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasSpawnableLoadBehavior loadBehavior, uint32_t tag, @@ -216,17 +223,58 @@ namespace AzToolsFramework::Prefab::SpawnableUtils { using namespace AzToolsFramework::Prefab::PrefabConversionUtils; - AliasPath alias = source.GetAliasPathRelativeToInstance(entity); + AliasPath alias = source.GetAliasPathRelativeToInstance(entityId); if (!alias.empty()) { - auto&& [replacement, storedAliasType] = Internal::ApplyAlias(source, entity, aliasType); + auto&& [replacement, storedAliasType] = Internal::ApplyAlias(source, entityId, aliasType); + if (replacement) + { + AZ::Entity* result = replacement.get(); + target.GetEntities().push_back(AZStd::move(replacement)); + EntityAliasStore store; + store.m_aliasType = storedAliasType; + store.m_source.emplace(AZStd::move(sourcePrefabName), AZStd::move(alias)); + store.m_target.emplace(target, result->GetId()); + store.m_tag = tag; + store.m_loadBehavior = loadBehavior; + context.RegisterSpawnableEntityAlias(AZStd::move(store)); + + return result; + } + else + { + AZ_Assert(false, "A replacement for entity with id %zu could not be created.", static_cast(entityId)); + return nullptr; + } + } + else + { + AZ_Assert(false, "Entity with id %llu was not found in the source prefab.", static_cast(entityId)); + return nullptr; + } + } + + AZ::Entity* CreateEntityAlias( + AzFramework::Spawnable& source, + AzFramework::Spawnable& target, + AZ::EntityId entityId, + AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasType aliasType, + AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasSpawnableLoadBehavior loadBehavior, + uint32_t tag, + AzToolsFramework::Prefab::PrefabConversionUtils::PrefabProcessorContext& context) + { + using namespace AzToolsFramework::Prefab::PrefabConversionUtils; + + auto&& [replacement, storedAliasType] = Internal::ApplyAlias(source, entityId, aliasType); + if (replacement) + { AZ::Entity* result = replacement.get(); target.GetEntities().push_back(AZStd::move(replacement)); - + EntityAliasStore store; store.m_aliasType = storedAliasType; - store.m_source.emplace(AZStd::move(sourcePrefabName), AZStd::move(alias)); + store.m_source.emplace(source, entityId); store.m_target.emplace(target, result->GetId()); store.m_tag = tag; store.m_loadBehavior = loadBehavior; @@ -236,37 +284,11 @@ namespace AzToolsFramework::Prefab::SpawnableUtils } else { - AZ_Assert(false, "Entity with id %llu was not found in the source prefab.", static_cast(entity)); + AZ_Assert(false, "A replacement for entity with id %zu could not be created.", static_cast(entityId)); return nullptr; } } - AZ::Entity* CreateEntityAlias( - AzFramework::Spawnable& source, - AzFramework::Spawnable& target, - AZ::EntityId entity, - AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasType aliasType, - AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasSpawnableLoadBehavior loadBehavior, - uint32_t tag, - AzToolsFramework::Prefab::PrefabConversionUtils::PrefabProcessorContext& context) - { - using namespace AzToolsFramework::Prefab::PrefabConversionUtils; - - auto&& [replacement, storedAliasType] = Internal::ApplyAlias(source, entity, aliasType); - AZ::Entity* result = replacement.get(); - target.GetEntities().push_back(AZStd::move(replacement)); - - EntityAliasStore store; - store.m_aliasType = storedAliasType; - store.m_source.emplace(source, entity); - store.m_target.emplace(target, result->GetId()); - store.m_tag = tag; - store.m_loadBehavior = loadBehavior; - context.RegisterSpawnableEntityAlias(AZStd::move(store)); - - return result; - } - uint32_t FindEntityIndex(AZ::EntityId entity, const AzFramework::Spawnable& spawnable) { auto begin = spawnable.GetEntities().begin(); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.h index 892b83455d..ea8a49857e 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.h @@ -36,7 +36,7 @@ namespace AzToolsFramework::Prefab::SpawnableUtils AzToolsFramework::Prefab::Instance& source, AZStd::string targetPrefabName, AzToolsFramework::Prefab::Instance& target, - AZ::EntityId entity, + AZ::EntityId entityId, AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasType aliasType, AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasSpawnableLoadBehavior loadBehavior, uint32_t tag, @@ -45,7 +45,7 @@ namespace AzToolsFramework::Prefab::SpawnableUtils AZStd::string sourcePrefabName, AzToolsFramework::Prefab::Instance& source, AzFramework::Spawnable& target, - AZ::EntityId entity, + AZ::EntityId entityId, AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasType aliasType, AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasSpawnableLoadBehavior loadBehavior, uint32_t tag, @@ -53,7 +53,7 @@ namespace AzToolsFramework::Prefab::SpawnableUtils AZ::Entity* CreateEntityAlias( AzFramework::Spawnable& source, AzFramework::Spawnable& target, - AZ::EntityId entity, + AZ::EntityId entityId, AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasType aliasType, AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasSpawnableLoadBehavior loadBehavior, uint32_t tag, From 13e83e948809474387295ed2e7061ecadb1682fd Mon Sep 17 00:00:00 2001 From: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> Date: Mon, 8 Nov 2021 13:34:12 -0800 Subject: [PATCH 11/14] Build fix. Signed-off-by: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> --- .../AzFramework/Spawnable/Spawnable.cpp | 26 ++++++++++++------- .../AzFramework/Spawnable/Spawnable.h | 2 ++ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp index 27e8a85597..6259a57ab5 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp @@ -532,21 +532,29 @@ namespace AzFramework void Spawnable::Reflect(AZ::ReflectContext* context) { + EntityAlias::Reflect(context); + if (auto* serializeContext = azrtti_cast(context); serializeContext != nullptr) { - serializeContext->Class() - ->Version(1) - ->Field("Spawnable", &Spawnable::EntityAlias::m_spawnable) - ->Field("Tag", &Spawnable::EntityAlias::m_tag) - ->Field("Source Index", &Spawnable::EntityAlias::m_sourceIndex) - ->Field("Target Index", &Spawnable::EntityAlias::m_targetIndex) - ->Field("Alias Type", &Spawnable::EntityAlias::m_aliasType) - ->Field("Queue Load", &Spawnable::EntityAlias::m_queueLoad); - serializeContext->Class()->Version(2) ->Field("Meta data", &Spawnable::m_metaData) ->Field("Entity aliases", &Spawnable::m_entityAliases) ->Field("Entities", &Spawnable::m_entities); } } + + void Spawnable::EntityAlias::Reflect(AZ::ReflectContext* context) + { + if (auto* serializeContext = azrtti_cast(context); serializeContext != nullptr) + { + serializeContext->Class() + ->Version(1) + ->Field("Spawnable", &EntityAlias::m_spawnable) + ->Field("Tag", &EntityAlias::m_tag) + ->Field("Source Index", &EntityAlias::m_sourceIndex) + ->Field("Target Index", &EntityAlias::m_targetIndex) + ->Field("Alias Type", &EntityAlias::m_aliasType) + ->Field("Queue Load", &EntityAlias::m_queueLoad); + } + } } // namespace AzFramework diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h index f0aa2c7806..f029246847 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h @@ -62,6 +62,8 @@ namespace AzFramework uint32_t m_targetIndex{ 0 }; //!< The index of the entity in the target spawnable that will be used to replace the original. EntityAliasType m_aliasType{ EntityAliasType::Original }; //!< The kind of replacement. bool m_queueLoad{ false }; //!< Whether or not to automatically queue the spawnable for loading. + + static void Reflect(AZ::ReflectContext* context); }; using EntityList = AZStd::vector>; From 976c6abb9089026ea164c5fd8c5852df2d5bd407 Mon Sep 17 00:00:00 2001 From: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> Date: Mon, 8 Nov 2021 13:34:12 -0800 Subject: [PATCH 12/14] Build fix. Signed-off-by: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> --- .../AzFramework/Spawnable/Spawnable.cpp | 26 ++++++++++++------- .../AzFramework/Spawnable/Spawnable.h | 2 ++ .../Spawnable/SpawnableAssetHandler.cpp | 2 +- .../Spawnable/SpawnableEntitiesManager.cpp | 2 -- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp index 27e8a85597..6259a57ab5 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp @@ -532,21 +532,29 @@ namespace AzFramework void Spawnable::Reflect(AZ::ReflectContext* context) { + EntityAlias::Reflect(context); + if (auto* serializeContext = azrtti_cast(context); serializeContext != nullptr) { - serializeContext->Class() - ->Version(1) - ->Field("Spawnable", &Spawnable::EntityAlias::m_spawnable) - ->Field("Tag", &Spawnable::EntityAlias::m_tag) - ->Field("Source Index", &Spawnable::EntityAlias::m_sourceIndex) - ->Field("Target Index", &Spawnable::EntityAlias::m_targetIndex) - ->Field("Alias Type", &Spawnable::EntityAlias::m_aliasType) - ->Field("Queue Load", &Spawnable::EntityAlias::m_queueLoad); - serializeContext->Class()->Version(2) ->Field("Meta data", &Spawnable::m_metaData) ->Field("Entity aliases", &Spawnable::m_entityAliases) ->Field("Entities", &Spawnable::m_entities); } } + + void Spawnable::EntityAlias::Reflect(AZ::ReflectContext* context) + { + if (auto* serializeContext = azrtti_cast(context); serializeContext != nullptr) + { + serializeContext->Class() + ->Version(1) + ->Field("Spawnable", &EntityAlias::m_spawnable) + ->Field("Tag", &EntityAlias::m_tag) + ->Field("Source Index", &EntityAlias::m_sourceIndex) + ->Field("Target Index", &EntityAlias::m_targetIndex) + ->Field("Alias Type", &EntityAlias::m_aliasType) + ->Field("Queue Load", &EntityAlias::m_queueLoad); + } + } } // namespace AzFramework diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h index f0aa2c7806..f029246847 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h @@ -62,6 +62,8 @@ namespace AzFramework uint32_t m_targetIndex{ 0 }; //!< The index of the entity in the target spawnable that will be used to replace the original. EntityAliasType m_aliasType{ EntityAliasType::Original }; //!< The kind of replacement. bool m_queueLoad{ false }; //!< Whether or not to automatically queue the spawnable for loading. + + static void Reflect(AZ::ReflectContext* context); }; using EntityList = AZStd::vector>; diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableAssetHandler.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableAssetHandler.cpp index eab681da0d..411ee56687 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableAssetHandler.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableAssetHandler.cpp @@ -97,7 +97,7 @@ namespace AzFramework void SpawnableAssetHandler::ResolveEntityAliases( Spawnable* spawnable, - const AZ::Data::Asset& asset, + [[maybe_unused]] const AZ::Data::Asset& asset, AZStd::chrono::milliseconds streamingDeadline, AZ::IO::IStreamerTypes::Priority streamingPriority, const AZ::Data::AssetFilterCB& assetLoadFilterCB) diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp index 1e863877a4..37570caec7 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp @@ -317,8 +317,6 @@ namespace AzFramework AZ::Entity* previouslySpawnedEntity, AZ::SerializeContext& serializeContext) { - using ResultType = AZStd::pair; - AZ::Entity* clone = nullptr; switch (alias.m_aliasType) { From b3295ffeb3504bca26213bb6b9a28a138c83e7ba Mon Sep 17 00:00:00 2001 From: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> Date: Mon, 8 Nov 2021 14:54:43 -0800 Subject: [PATCH 13/14] Fixed several issues with compilation of Spawnable Entities Aliases. Signed-off-by: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> --- .../Tests/Mocks/MockSpawnableEntitiesInterface.h | 10 +++++++++- .../Prefab/Spawnable/SpawnableUtils.cpp | 3 ++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Code/Framework/AzFramework/Tests/Mocks/MockSpawnableEntitiesInterface.h b/Code/Framework/AzFramework/Tests/Mocks/MockSpawnableEntitiesInterface.h index a437545adf..4d04fa5bc9 100644 --- a/Code/Framework/AzFramework/Tests/Mocks/MockSpawnableEntitiesInterface.h +++ b/Code/Framework/AzFramework/Tests/Mocks/MockSpawnableEntitiesInterface.h @@ -36,7 +36,7 @@ namespace AzFramework MOCK_METHOD3( SpawnEntities, - void(EntitySpawnTicket& ticket, AZStd::vector entityIndices, SpawnEntitiesOptionalArgs optionalArgs)); + void(EntitySpawnTicket& ticket, AZStd::vector entityIndices, SpawnEntitiesOptionalArgs optionalArgs)); MOCK_METHOD2(DespawnAllEntities, void(EntitySpawnTicket& ticket, DespawnAllEntitiesOptionalArgs optionalArgs)); @@ -49,6 +49,13 @@ namespace AzFramework ReloadSpawnable, void(EntitySpawnTicket& ticket, AZ::Data::Asset spawnable, ReloadSpawnableOptionalArgs optionalArgs)); + MOCK_METHOD3( + UpdateEntityAliasTypes, + void( + EntitySpawnTicket& ticket, + AZStd::vector updatedAliases, + UpdateEntityAliasTypesOptionalArgs optionalArgs)); + MOCK_METHOD3( ListEntities, void(EntitySpawnTicket& ticket, ListEntitiesCallback listCallback, ListEntitiesOptionalArgs optionalArgs)); @@ -61,6 +68,7 @@ namespace AzFramework void(EntitySpawnTicket& ticket, ClaimEntitiesCallback listCallback, ClaimEntitiesOptionalArgs optionalArgs)); MOCK_METHOD3(Barrier, void(EntitySpawnTicket& ticket, BarrierCallback completionCallback, BarrierOptionalArgs optionalArgs)); + MOCK_METHOD3(LoadBarrier, void(EntitySpawnTicket& ticket, BarrierCallback completionCallback, LoadBarrierOptionalArgs optionalArgs)); MOCK_METHOD1(CreateTicket, AZStd::pair(AZ::Data::Asset&& spawnable)); MOCK_METHOD1(DestroyTicket, void(void* ticket)); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.cpp index ab3c52f6d0..5e552aad3f 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.cpp @@ -8,6 +8,7 @@ #include +#include #include #include #include @@ -297,7 +298,7 @@ namespace AzToolsFramework::Prefab::SpawnableUtils { if ((*it)->GetId() == entity) { - return AZStd::distance(begin, it); + return aznumeric_caster(AZStd::distance(begin, it)); } } return InvalidEntityIndex; From cc2513f224bf2d42fa268c50040ec82d5f88e04c Mon Sep 17 00:00:00 2001 From: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> Date: Tue, 9 Nov 2021 09:46:32 -0800 Subject: [PATCH 14/14] Linux build fix Spawnable Entity Aliases. Signed-off-by: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> --- .../AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.h index f20c85eb1d..d35a09a574 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.h @@ -50,7 +50,6 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils struct EntityAliasSpawnableLink { - EntityAliasSpawnableLink() = default; EntityAliasSpawnableLink(AzFramework::Spawnable& spawnable, AZ::EntityId index); AzFramework::Spawnable& m_spawnable; @@ -59,7 +58,6 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils struct EntityAliasPrefabLink { - EntityAliasPrefabLink() = default; EntityAliasPrefabLink(AZStd::string prefabName, AzToolsFramework::Prefab::AliasPath alias); AZStd::string m_prefabName;