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