Merge pull request #5108 from aws-lumberyard-dev/Prefabs/SpawnableEntityAlias

Ability to setup aliases for entities spawned from spawnables.
This commit is contained in:
Ronald Koppers
2021-11-09 13:17:59 -08:00
committed by GitHub
32 changed files with 3099 additions and 357 deletions
@@ -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.
@@ -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<Spawnable> 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<Spawnable> 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) {}
};
@@ -6,12 +6,473 @@
*
*/
#include <AzCore/Asset/AssetSerializer.h>
#include <AzCore/RTTI/ReflectContext.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/std/numeric.h>
#include <AzCore/std/sort.h>
#include <AzCore/std/typetraits/typetraits.h>
#include <AzFramework/Spawnable/Spawnable.h>
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::IsValid(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_queueLoad ||
alias.m_aliasType == Spawnable::EntityAliasType::Original ||
alias.m_aliasType == Spawnable::EntityAliasType::Disable)
{
continue;
}
if (!alias.m_spawnable.IsReady() && !alias.m_spawnable.IsError())
{
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<AZ::Data::AssetId> spawnableIds;
for (const Spawnable::EntityAlias& alias : *aliases)
{
// 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())
{
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<AZ::Data::AssetId> spawnableIds;
for (const Spawnable::EntityAlias& alias : *aliases)
{
// 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())
{
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 (IsValid())
{
Optimize();
AZ_Assert(
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;
}
}
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();
new(this) EntityAliasVisitor(AZStd::move(rhs));
}
return *this;
}
bool Spawnable::EntityAliasVisitor::IsValid() const
{
return EntityAliasVisitorBase::IsValid(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<Spawnable> 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::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)
{
if (alias.m_queueLoad &&
alias.m_aliasType != Spawnable::EntityAliasType::Original &&
alias.m_aliasType != Spawnable::EntityAliasType::Disable &&
!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<Spawnable> 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<Spawnable> 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.
uint32_t previousIndex = AZStd::numeric_limits<uint32_t>::max();
Spawnable::EntityAliasType previousType =
static_cast<Spawnable::EntityAliasType>(AZStd::numeric_limits<AZStd::underlying_type_t<Spawnable::EntityAliasType>>::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:
[[fallthrough]];
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::Disable ||
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.
it = m_entityAliasList->erase(it - 1) + 1;
end = m_entityAliasList->end();
}
else
{
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 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<Spawnable>({}, azrtti_typeid<Spawnable>());
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;
previousIndex = it->m_sourceIndex;
previousType = it->m_aliasType;
// Insert to maintain the order.
it = m_entityAliasList->insert(it, AZStd::move(insert));
it += 2;
end = m_entityAliasList->end();
}
else
{
previousType = it->m_aliasType;
++it;
}
break;
default:
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;
}
}
//
// EntityAliasConstVisitor
//
Spawnable::EntityAliasConstVisitor::EntityAliasConstVisitor(const Spawnable& owner, const EntityAliasList* entityAliasList)
: m_owner(owner)
, m_entityAliasList(entityAliasList)
{
}
Spawnable::EntityAliasConstVisitor::~EntityAliasConstVisitor()
{
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).",
m_owner.m_shareState.load());
m_owner.m_shareState++;
}
}
bool Spawnable::EntityAliasConstVisitor::IsValid() const
{
return EntityAliasVisitorBase::IsValid(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,6 +488,33 @@ namespace AzFramework
return m_entities;
}
auto Spawnable::TryGetAliasesConst() const -> EntityAliasConstVisitor
{
int32_t expected = ShareState::NotShared;
do
{
// Try to set the lock to a negative number to indicate a shared read.
if (m_shareState.compare_exchange_strong(expected, expected - 1))
{
return EntityAliasConstVisitor(*this, &m_entityAliases);
}
// 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);
}
auto Spawnable::TryGetAliases() const -> EntityAliasConstVisitor
{
return TryGetAliasesConst();
}
auto Spawnable::TryGetAliases() -> EntityAliasVisitor
{
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
{
return m_entities.empty();
@@ -44,11 +532,29 @@ namespace AzFramework
void Spawnable::Reflect(AZ::ReflectContext* context)
{
EntityAlias::Reflect(context);
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context); serializeContext != nullptr)
{
serializeContext->Class<Spawnable, AZ::Data::AssetData>()->Version(1)
serializeContext->Class<Spawnable, AZ::Data::AssetData>()->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<AZ::SerializeContext*>(context); serializeContext != nullptr)
{
serializeContext->Class<Spawnable::EntityAlias>()
->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
@@ -11,6 +11,7 @@
#include <AzCore/Asset/AssetCommon.h>
#include <AzCore/Component/Entity.h>
#include <AzCore/Memory/SystemAllocator.h>
#include <AzCore/std/parallel/atomic.h>
#include <AzCore/std/containers/vector.h>
#include <AzCore/std/smart_ptr/unique_ptr.h>
#include <AzFramework/Spawnable/SpawnableMetaData.h>
@@ -29,7 +30,148 @@ 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.
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
//!< maintaining a valid component list.
};
enum ShareState : int32_t
{
Read = -1,
NotShared = 0,
ReadWrite = 1
};
//! 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<Spawnable> 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.
static void Reflect(AZ::ReflectContext* context);
};
using EntityList = AZStd::vector<AZStd::unique_ptr<AZ::Entity>>;
using EntityAliasList = AZStd::vector<EntityAlias>;
private:
class EntityAliasVisitorBase
{
protected:
bool IsValid(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<void(const AZ::Data::Asset<Spawnable>& 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;
//! Checks if the visitor was able to retrieve data. This needs to be checked before calling any other functions.
bool IsValid() const;
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;
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<Spawnable> targetSpawnable,
AZ::Crc32 tag,
uint32_t sourceIndex,
uint32_t targetIndex,
Spawnable::EntityAliasType aliasType,
bool queueLoad);
using ListSpawnablesRequiringLoadCallback = AZStd::function<void(AZ::Data::Asset<Spawnable>& spawnablePendingLoad)>;
void ListSpawnablesRequiringLoad(const ListSpawnablesRequiringLoadCallback& callback);
using UpdateCallback = AZStd::function<void(
Spawnable::EntityAliasType& aliasType,
bool& queueLoad,
const AZ::Data::Asset<Spawnable>& 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* entityAliasList);
~EntityAliasConstVisitor();
//! Checks if the visitor was able to retrieve data. This needs to be checked before calling any other functions.
bool IsValid() 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";
@@ -45,6 +187,9 @@ namespace AzFramework
const EntityList& GetEntities() const;
EntityList& GetEntities();
EntityAliasConstVisitor TryGetAliasesConst() const;
EntityAliasConstVisitor TryGetAliases() const;
EntityAliasVisitor TryGetAliases();
bool IsEmpty() const;
SpawnableMetaData& GetMetaData();
@@ -55,11 +200,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<int32_t> m_shareState{ ShareState::NotShared };
};
using SpawnableList = AZStd::vector<Spawnable>;
} // namespace AzFramework
@@ -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 <AzCore/EBus/EBus.h>
#include <AzCore/std/parallel/mutex.h>
#include <AzFramework/Spawnable/Spawnable.h>
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<SpawnableAssetEvents>;
} // namespace AzFramework
@@ -9,8 +9,10 @@
#include <AzCore/Casting/lossy_cast.h>
#include <AzCore/Serialization/Utils.h>
#include <AzCore/std/string/string.h>
#include <AzCore/std/sort.h>
#include <AzFramework/Spawnable/Spawnable.h>
#include <AzFramework/Spawnable/SpawnableAssetHandler.h>
#include <AzFramework/Spawnable/SpawnableAssetBus.h>
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,41 @@ namespace AzFramework
AZ::Uuid subIdHash = AZ::Uuid::CreateData(id.data(), id.size());
return azlossy_caster(subIdHash.GetHash());
}
void SpawnableAssetHandler::ResolveEntityAliases(
Spawnable* spawnable,
[[maybe_unused]] const AZ::Data::Asset<AZ::Data::AssetData>& asset,
AZStd::chrono::milliseconds streamingDeadline,
AZ::IO::IStreamerTypes::Priority streamingPriority,
const AZ::Data::AssetFilterCB& assetLoadFilterCB)
{
Spawnable::EntityAliasVisitor aliases = spawnable->TryGetAliases();
AZ_Assert(aliases.IsValid(), "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());
// The aliases will only be optimized if OnResolveAliases has made any changes.
aliases.Optimize();
aliases.ListSpawnablesRequiringLoad(
[&assetLoadFilterCB, streamingDeadline, streamingPriority](AZ::Data::Asset<Spawnable>& assetPendingLoad)
{
AZ::Data::AssetLoadParameters loadInfo;
loadInfo.m_assetLoadFilterCB = assetLoadFilterCB;
loadInfo.m_deadline = streamingDeadline;
loadInfo.m_priority = streamingPriority;
assetPendingLoad.QueueLoad(loadInfo);
});
}
}
} // namespace AzFramework
@@ -50,5 +50,13 @@ namespace AzFramework
const AZ::Data::Asset<AZ::Data::AssetData>& asset,
AZStd::shared_ptr<AZ::Data::AssetDataStream> stream,
const AZ::Data::AssetFilterCB& assetLoadFilterCB) override;
private:
void ResolveEntityAliases(
class Spawnable* spawnable,
const AZ::Data::Asset<AZ::Data::AssetData>& asset,
AZStd::chrono::milliseconds streamingDeadline,
AZ::IO::IStreamerTypes::Priority streamingPriority,
const AZ::Data::AssetFilterCB& assetLoadFilterCB);
};
} // namespace AzFramework
@@ -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<size_t> entityIndices)
void SpawnableEntitiesContainer::SpawnEntities(AZStd::vector<uint32_t> 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> spawnable)
@@ -36,6 +36,12 @@ namespace AzFramework
public:
using AlertCallback = AZStd::function<void(uint32_t generation)>;
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<size_t> entityIndices);
void SpawnEntities(AZStd::vector<uint32_t> 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);
//! @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.
//! @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);
private:
void Connect(AZ::Data::Asset<Spawnable> spawnable);
@@ -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)
{
@@ -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<void(EntitySpawnTicket::Id)>;
using RetrieveEntitySpawnTicketCallback = AZStd::function<void(EntitySpawnTicket*)>;
using ReloadSpawnableCallback = AZStd::function<void(EntitySpawnTicket::Id, SpawnableConstEntityContainerView)>;
using UpdateEntityAliasTypesCallback = AZStd::function<void(EntitySpawnTicket::Id)>;
using ListEntitiesCallback = AZStd::function<void(EntitySpawnTicket::Id, SpawnableConstEntityContainerView)>;
using ListIndicesEntitiesCallback = AZStd::function<void(EntitySpawnTicket::Id, SpawnableConstIndexEntityContainerView)>;
using ClaimEntitiesCallback = AZStd::function<void(EntitySpawnTicket::Id, SpawnableEntityContainerView)>;
@@ -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<size_t> entityIndices, SpawnEntitiesOptionalArgs optionalArgs = {}) = 0;
EntitySpawnTicket& ticket, AZStd::vector<uint32_t> 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> 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<EntityAliasTypeChange> 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<EntitySpawnTicket::Id, void*> CreateTicket(AZ::Data::Asset<Spawnable>&& spawnable) = 0;
virtual void DestroyTicket(void* ticket) = 0;
template<typename T>
static T& GetTicketPayload(EntitySpawnTicket& ticket)
[[nodiscard]] static T& GetTicketPayload(EntitySpawnTicket& ticket)
{
return *reinterpret_cast<T*>(ticket.m_payload);
}
template<typename T>
static const T& GetTicketPayload(const EntitySpawnTicket& ticket)
[[nodiscard]] static const T& GetTicketPayload(const EntitySpawnTicket& ticket)
{
return *reinterpret_cast<const T*>(ticket.m_payload);
}
template<typename T>
static T* GetTicketPayload(EntitySpawnTicket* ticket)
[[nodiscard]] static T* GetTicketPayload(EntitySpawnTicket* ticket)
{
return reinterpret_cast<T*>(ticket->m_payload);
}
template<typename T>
static const T* GetTicketPayload(const EntitySpawnTicket* ticket)
[[nodiscard]] static const T* GetTicketPayload(const EntitySpawnTicket* ticket)
{
return reinterpret_cast<const T*>(ticket->m_payload);
}
@@ -60,7 +60,7 @@ namespace AzFramework
}
void SpawnableEntitiesManager::SpawnEntities(
EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices, SpawnEntitiesOptionalArgs optionalArgs)
EntitySpawnTicket& ticket, AZStd::vector<uint32_t> 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<EntityAliasTypeChange> 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));
}
@@ -273,14 +300,73 @@ 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<AZ::EntityId, allowDuplicateIds>::CloneObjectAndGenerateNewIdsAndFixRefs(
&entityTemplate, templateToCloneMap, &serializeContext);
&entityPrototype, prototypeToCloneMap, &serializeContext);
}
AZ::Entity* SpawnableEntitiesManager::CloneSingleAliasedEntity(
const AZ::Entity& entityPrototype,
const Spawnable::EntityAlias& alias,
EntityIdMap& prototypeToCloneMap,
AZ::Entity* previouslySpawnedEntity,
AZ::SerializeContext& serializeContext)
{
AZ::Entity* clone = nullptr;
switch (alias.m_aliasType)
{
case Spawnable::EntityAliasType::Original:
// Behave as the original version.
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]), 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]), 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(), prototypeToCloneMap,
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& 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 : componentPrototypes)
{
AZ::Component* clone = AZ::IdUtils::Remapper<AZ::EntityId, allowDuplicateIds>::CloneObjectAndGenerateNewIdsAndFixRefs(
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());
}
}
void SpawnableEntitiesManager::InitializeEntityIdMappings(
@@ -316,161 +402,264 @@ 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<AZ::Entity*>& spawnedEntities = ticket.m_spawnedEntities;
AZStd::vector<size_t>& 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.IsValid() && 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<AZ::Entity*>& spawnedEntities = ticket.m_spawnedEntities;
AZStd::vector<uint32_t>& 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 'prototype' 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<AZ::Entity*>& spawnedEntities = ticket.m_spawnedEntities;
AZStd::vector<size_t>& 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)
{
spawnedEntities.emplace_back(
CloneSingleEntity(*entitiesToSpawn[i], ticket.m_entityIdReferenceMap, *request.m_serializeContext));
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);
previousEntity = clone;
if (clone)
{
spawnedEntities.emplace_back(clone);
spawnedEntityIndices.push_back(i);
}
++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)
{
(*it)->SetSpawnTicketId(request.m_ticketId);
GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, *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;
if (request.m_completionCallback)
{
request.m_completionCallback(request.m_ticketId, SpawnableConstEntityContainerView(
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
}
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));
}
ticket.m_currentRequestId++;
return true;
}
else
{
return false;
// Add to the game context, now the entities are active
for (auto it = newEntitiesBegin; it != newEntitiesEnd; ++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.
if (request.m_completionCallback)
{
request.m_completionCallback(request.m_ticketId, SpawnableConstEntityContainerView(newEntitiesBegin, newEntitiesEnd));
}
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.IsValid() && aliases.AreAllSpawnablesReady())
{
AZStd::vector<AZ::Entity*>& spawnedEntities = ticket.m_spawnedEntities;
AZStd::vector<uint32_t>& 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 '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 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.
// 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);
spawnedEntities.push_back(
CloneSingleEntity(*entitiesToSpawn[index], ticket.m_entityIdReferenceMap, *request.m_serializeContext));
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 || aliasIt->m_sourceIndex != index)
{
spawnedEntities.emplace_back(
CloneSingleEntity(*entitiesToSpawn[index], ticket.m_entityIdReferenceMap, *request.m_serializeContext));
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);
previousEntity = clone;
if (clone)
{
spawnedEntities.emplace_back(clone);
spawnedEntityIndices.push_back(index);
}
++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)
{
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)
{
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 +684,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 +718,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(),
@@ -564,7 +753,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)
@@ -574,7 +763,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 +779,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 +805,40 @@ 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.IsValid())
{
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;
}
}
return CommandResult::Requeue;
}
auto SpawnableEntitiesManager::ProcessRequest(ListEntitiesCommand& request) -> CommandResult
{
Ticket& ticket = *request.m_ticket;
if (request.m_requestId == ticket.m_currentRequestId)
@@ -632,15 +846,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 +865,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 +885,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 +904,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.IsValid() || !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)
{
@@ -706,19 +944,24 @@ namespace AzFramework
{
if (entity != nullptr)
{
// 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;
return true;
return CommandResult::Executed;
}
else
{
return false;
return CommandResult::Requeue;
}
}
} // namespace AzFramework
@@ -55,13 +55,18 @@ namespace AzFramework
void SpawnAllEntities(EntitySpawnTicket& ticket, SpawnAllEntitiesOptionalArgs optionalArgs = {}) override;
void SpawnEntities(
EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices, SpawnEntitiesOptionalArgs optionalArgs = {}) override;
EntitySpawnTicket& ticket, AZStd::vector<uint32_t> 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> spawnable, ReloadSpawnableOptionalArgs optionalArgs = {}) override;
void UpdateEntityAliasTypes(
EntitySpawnTicket& ticket,
AZStd::vector<EntityAliasTypeChange> 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,14 +85,20 @@ 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<uint32_t>::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
@@ -100,14 +113,14 @@ namespace AzFramework
AZStd::unordered_set<AZ::EntityId> m_previouslySpawned;
AZStd::vector<AZ::Entity*> m_spawnedEntities;
AZStd::vector<size_t> m_spawnedEntityIndices;
AZStd::vector<uint32_t> m_spawnedEntityIndices;
AZ::Data::Asset<Spawnable> 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<size_t> m_entityIndices;
AZStd::vector<uint32_t> 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<Spawnable> 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<EntityAliasTypeChange> 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
@@ -212,18 +243,31 @@ 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& entityPrototype,
const Spawnable::EntityAlias& alias,
EntityIdMap& prototypeToCloneMap,
AZ::Entity* previouslySpawnedEntity,
AZ::SerializeContext& serializeContext);
void AppendComponents(
AZ::Entity& target,
const AZ::Entity::ComponentArrayType& componentPrototypes,
EntityIdMap& prototypeToCloneMap,
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
@@ -72,7 +72,7 @@ namespace AzFramework
uint64_t SpawnableSystemComponent::AssignRootSpawnable(AZ::Data::Asset<Spawnable> 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<Spawnable> 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);
@@ -82,6 +82,7 @@ namespace AzFramework
//
void OnRootSpawnableAssigned(AZ::Data::Asset<Spawnable> rootSpawnable, uint32_t generation) override;
void OnRootSpawnableReady(AZ::Data::Asset<Spawnable> rootSpawnable, uint32_t generation) override;
void OnRootSpawnableReleased(uint32_t generation) override;
protected:
@@ -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
@@ -36,7 +36,7 @@ namespace AzFramework
MOCK_METHOD3(
SpawnEntities,
void(EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices, SpawnEntitiesOptionalArgs optionalArgs));
void(EntitySpawnTicket& ticket, AZStd::vector<uint32_t> 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> spawnable, ReloadSpawnableOptionalArgs optionalArgs));
MOCK_METHOD3(
UpdateEntityAliasTypes,
void(
EntitySpawnTicket& ticket,
AZStd::vector<EntityAliasTypeChange> 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<EntitySpawnTicket::Id, void*>(AZ::Data::Asset<Spawnable>&& spawnable));
MOCK_METHOD1(DestroyTicket, void(void* ticket));
@@ -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<AZ::SerializeContext*>(reflection))
{
serializeContext->Class<SourceSpawnableComponent, AZ::Component>();
}
}
};
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<AZ::SerializeContext*>(reflection))
{
serializeContext->Class<TargetSpawnableComponent, AZ::Component>();
}
}
};
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,10 +145,126 @@ namespace UnitTest
entities.reserve(numElements);
for (size_t i=0; i<numElements; ++i)
{
entities.push_back(AZStd::make_unique<AZ::Entity>());
auto entry = AZStd::make_unique<AZ::Entity>();
entry->AddComponent(aznew SourceSpawnableComponent());
entities.push_back(AZStd::move(entry));
}
}
AZ::Data::Asset<AzFramework::Spawnable> 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<AZ::Entity>();
entry->AddComponent(aznew TargetSpawnableComponent());
entities.push_back(AZStd::move(entry));
}
return AZ::Data::Asset<AzFramework::Spawnable>(target, AZ::Data::AssetLoadBehavior::NoLoad);
}
template<size_t AliasCount>
void InsertEntityAliases(
const AZStd::array<uint32_t, AliasCount>& sourceIds,
const AZStd::array<uint32_t, AliasCount>& targetIds,
const AZStd::array<AzFramework::Spawnable::EntityAliasType, AliasCount>& aliasTypes,
AZ::Data::Asset<AzFramework::Spawnable>* 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<AzFramework::Spawnable> spawnable(
AZ::Data::AssetId(AZ::Uuid("{4CBEC17A-52D6-42D5-9037-F4C05B9CE1D9}"), i), azrtti_typeid<AzFramework::Spawnable>());
visitor.AddAlias(AZStd::move(spawnable), AZ::Crc32(i), sourceIds[i], targetIds[i], aliasTypes[i], false);
}
}
}
static bool AreAllEntitiesReplaced(AzFramework::SpawnableConstEntityContainerView entities)
{
for (const AZ::Entity* entity : entities)
{
if (entity)
{
if (entity->FindComponent<SourceSpawnableComponent>() != nullptr ||
entity->FindComponent<TargetSpawnableComponent>() == 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<SourceSpawnableComponent>() == nullptr ||
entity->FindComponent<TargetSpawnableComponent>() != nullptr)
{
return false;
}
}
else
{
if (entity->FindComponent<SourceSpawnableComponent>() != nullptr ||
entity->FindComponent<TargetSpawnableComponent>() == 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<SourceSpawnableComponent>() == nullptr ||
entity->FindComponent<TargetSpawnableComponent>() == nullptr)
{
return false;
}
}
else
{
return false;
}
}
return true;
}
void CreateRecursiveHierarchy()
{
AzFramework::Spawnable::EntityList& entities = m_spawnable->GetEntities();
@@ -245,6 +397,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 +542,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 +551,135 @@ 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<NumEntities>(
{ 0, 1, 2, 3 }, { 0, 1, 2, 3 },
{ 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)
{
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::Disable, Spawnable::EntityAliasType::Disable });
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<Spawnable> 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 = false;
auto callback = [&spawnedEntitiesCount, &allReplaced](
AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
spawnedEntitiesCount += entities.size();
allReplaced = AreAllEntitiesReplaced(entities);
};
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<Spawnable> 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 allAdded = false;
auto callback = [&spawnedEntitiesCount, &allAdded](
AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
spawnedEntitiesCount += entities.size();
allAdded = IsEveryOtherEntityAReplacement(entities);
};
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(allAdded);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_AllAliasesWithMerge_SourceAndTargetComponentsMerged)
{
using namespace AzFramework;
static constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
AZ::Data::Asset<Spawnable> 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 allMerged = false;
auto callback = [&spawnedEntitiesCount, &allMerged](
AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
spawnedEntitiesCount += entities.size();
allMerged = AreAllMerged(entities);
};
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(allMerged);
}
//
// SpawnEntities
@@ -403,7 +690,7 @@ namespace UnitTest
static constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
AZStd::vector<size_t> indices = { 0, 2, 3, 1 };
AZStd::vector<uint32_t> indices = { 0, 2, 3, 1 };
size_t spawnedEntitiesCount = 0;
auto callback = [&spawnedEntitiesCount](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
@@ -423,7 +710,7 @@ namespace UnitTest
static constexpr size_t NumEntities = 1;
FillSpawnable(NumEntities);
AZStd::vector<size_t> indices = { 0, 0 };
AZStd::vector<uint32_t> indices = { 0, 0 };
size_t spawnedEntitiesCount = 0;
auto callback =
@@ -444,7 +731,7 @@ namespace UnitTest
static constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
AZStd::vector<size_t> indices = { 0, 2, 3, 1 };
AZStd::vector<uint32_t> indices = { 0, 2, 3, 1 };
size_t spawnedEntitiesCount = 0;
auto callback =
@@ -467,7 +754,7 @@ namespace UnitTest
FillSpawnable(NumEntities);
CreateSingleParent();
AZStd::vector<size_t> indices = { 0, 1, 2, 3 };
AZStd::vector<uint32_t> indices = { 0, 1, 2, 3 };
AZStd::vector<AZ::EntityId> parents;
auto callback = [&parents](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
@@ -499,7 +786,7 @@ namespace UnitTest
FillSpawnable(NumEntities);
CreateSingleParent();
AZStd::vector<size_t> indices = { 0, 1, 2, 3 };
AZStd::vector<uint32_t> indices = { 0, 1, 2, 3 };
AZStd::vector<AZ::EntityId> parents;
auto callback =
@@ -754,6 +1041,148 @@ 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<NumEntities>(
{ 0, 1, 2, 3 }, { 0, 1, 2, 3 },
{ Spawnable::EntityAliasType::Disable, Spawnable::EntityAliasType::Disable, Spawnable::EntityAliasType::Disable,
Spawnable::EntityAliasType::Disable });
AZStd::vector<uint32_t> 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::Disable, Spawnable::EntityAliasType::Disable, Spawnable::EntityAliasType::Disable });
AZStd::vector<uint32_t> 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<Spawnable> 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<uint32_t> indices = { 0, 2, 3, 1 };
size_t spawnedEntitiesCount = 0;
bool allReplaced = false;
auto callback = [&spawnedEntitiesCount, &allReplaced](
AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
spawnedEntitiesCount += entities.size();
allReplaced = AreAllEntitiesReplaced(entities);
};
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<Spawnable> 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<uint32_t> indices = { 0, 2, 3, 1 };
size_t spawnedEntitiesCount = 0;
bool allAdded = false;
auto callback =
[&spawnedEntitiesCount, &allAdded](
AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
spawnedEntitiesCount += entities.size();
allAdded = IsEveryOtherEntityAReplacement(entities);
};
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(allAdded);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_AllAliasesWithMerge_SourceAndTargetComponentsMerged)
{
using namespace AzFramework;
static constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
AZ::Data::Asset<Spawnable> 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<uint32_t> indices = { 0, 2, 3, 1 };
size_t spawnedEntitiesCount = 0;
bool allMerged = false;
auto callback = [&spawnedEntitiesCount, &allMerged](
AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
spawnedEntitiesCount += entities.size();
allMerged = AreAllMerged(entities);
};
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(allMerged);
}
//
// DespawnAllEntities
@@ -0,0 +1,513 @@
/*
* 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 <AzCore/Casting/numeric_cast.h>
#include <AzCore/UnitTest/TestTypes.h>
#include <AzFramework/Spawnable/Spawnable.h>
#include <AzTest/AzTest.h>
namespace UnitTest
{
class SpawnableTest : public AllocatorsFixture
{
public:
static constexpr size_t DefaultEntityAliasTestCount = 8;
void SetUp() override
{
AllocatorsFixture::SetUp();
m_spawnable = aznew AzFramework::Spawnable();
}
void TearDown() override
{
delete m_spawnable;
m_spawnable = nullptr;
AllocatorsFixture::TearDown();
}
void InsertEntities(size_t count)
{
AzFramework::Spawnable::EntityList& entities = m_spawnable->GetEntities();
entities.reserve(entities.size() + count);
for (size_t i = 0; i < count; ++i)
{
entities.emplace_back(AZStd::make_unique<AZ::Entity>());
}
}
template<size_t Count>
void InsertEntityAliases(
const AZStd::array<uint32_t, Count>& sourceIds,
const AZStd::array<uint32_t, Count>& targetIds,
const AZStd::array<AzFramework::Spawnable::EntityAliasType, Count>& aliasTypes,
bool queueLoad = false)
{
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
for (uint32_t i = 0; i < Count; ++i)
{
AZ::Data::Asset<AzFramework::Spawnable> spawnable(
AZ::Data::AssetId(AZ::Uuid("{4CBEC17A-52D6-42D5-9037-F4C05B9CE1D9}"), i), azrtti_typeid<AzFramework::Spawnable>());
visitor.AddAlias(spawnable, AZ::Crc32(i), sourceIds[i], targetIds[i], aliasTypes[i], queueLoad);
}
}
template<size_t Count>
void InsertEntityAliases(bool queueLoad)
{
using namespace AzFramework;
AZStd::array<uint32_t, Count> ids;
for (uint32_t i=0; i<aznumeric_cast<uint32_t>(Count); ++i)
{
ids[i] = i;
}
AZStd::array<AzFramework::Spawnable::EntityAliasType, Count> aliasTypes;
for (uint32_t i = 0; i < aznumeric_cast<uint32_t>(Count); ++i)
{
aliasTypes[i] = Spawnable::EntityAliasType::Replace;
}
InsertEntityAliases<Count>(ids, ids, aliasTypes, queueLoad);
}
template<size_t Count>
void InsertEntityAliases()
{
InsertEntityAliases<Count>(false);
}
protected:
AzFramework::Spawnable* m_spawnable;
};
//
// TryGetAliasesConst
//
TEST_F(SpawnableTest, TryGetAliasesConst_GetVisitor_VisitorDataIsAvailable)
{
AzFramework::Spawnable::EntityAliasConstVisitor visitor = m_spawnable->TryGetAliasesConst();
EXPECT_TRUE(visitor.IsValid());
}
TEST_F(SpawnableTest, TryGetAliasesConst_VisitorThatIsNotReadShared_VisitorDataIsNotAvailable)
{
AzFramework::Spawnable::EntityAliasVisitor readWriteVisitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(readWriteVisitor.IsValid());
AzFramework::Spawnable::EntityAliasConstVisitor visitor = m_spawnable->TryGetAliasesConst();
EXPECT_FALSE(visitor.IsValid());
}
TEST_F(SpawnableTest, TryGetAliasesConst_VisitorThatIsAlreadyReadShared_VisitorDataIsAvailable)
{
AzFramework::Spawnable::EntityAliasConstVisitor readVisitor = m_spawnable->TryGetAliasesConst();
ASSERT_TRUE(readVisitor.IsValid());
AzFramework::Spawnable::EntityAliasConstVisitor visitor = m_spawnable->TryGetAliasesConst();
EXPECT_TRUE(visitor.IsValid());
}
//
// TryGetAliases
//
TEST_F(SpawnableTest, TryGetAliases_GetVisitor_VisitorDataIsAvailable)
{
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
EXPECT_TRUE(visitor.IsValid());
}
TEST_F(SpawnableTest, TryGetAliasesConst_VisitorThatIsAlreadyShared_VisitorDataNotIsAvailable)
{
AzFramework::Spawnable::EntityAliasConstVisitor readVisitor = m_spawnable->TryGetAliasesConst();
ASSERT_TRUE(readVisitor.IsValid());
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
EXPECT_FALSE(visitor.IsValid());
}
//
// EntityAliasVisitor
//
//
// HasAliases
//
TEST_F(SpawnableTest, EntityAliasVisitor_HasAliases_EmptyAliasList_ReturnsFalse)
{
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
EXPECT_FALSE(visitor.HasAliases());
}
TEST_F(SpawnableTest, EntityAliasVisitor_HasAliases_FilledInAliasList_ReturnsTrue)
{
InsertEntities(8);
InsertEntityAliases<8>();
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
EXPECT_TRUE(visitor.HasAliases());
}
//
// Optimize
//
TEST_F(SpawnableTest, EntityAliasVisitor_Optimize_SortEntityAliases_AliasesAreSortedBySourceAndTargetId)
{
InsertEntities(DefaultEntityAliasTestCount);
InsertEntityAliases<DefaultEntityAliasTestCount>();
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
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.
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;
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.IsValid());
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;
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.IsValid());
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;
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.IsValid());
EXPECT_EQ(0, AZStd::distance(visitor.begin(), visitor.end()));
}
TEST_F(SpawnableTest, EntityAliasVisitor_Optimize_MixedOriginals_AllOriginalsRemoved)
{
using namespace AzFramework;
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.IsValid());
EXPECT_EQ(2, AZStd::distance(visitor.begin(), visitor.end()));
EXPECT_EQ(Spawnable::EntityAliasType::Disable, visitor.begin()->m_aliasType);
EXPECT_EQ(Spawnable::EntityAliasType::Replace, visitor.begin()[1].m_aliasType);
}
TEST_F(SpawnableTest, EntityAliasVisitor_Optimize_MergeAfterOriginal_NoAdditionalOriginalIsInserted)
{
using namespace AzFramework;
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.IsValid());
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;
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.IsValid());
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;
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.IsValid());
auto callback =
[](Spawnable::EntityAliasType& aliasType, bool& /*queueLoad*/, const AZ::Data::Asset<Spawnable>& /*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;
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.IsValid());
bool correctTag = false;
size_t numberOfUpdates = 0;
auto callback = [&correctTag, &numberOfUpdates](Spawnable::EntityAliasType& aliasType, bool& /*queueLoad*/,
const AZ::Data::Asset<Spawnable>& /*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;
InsertEntities(DefaultEntityAliasTestCount);
InsertEntityAliases<DefaultEntityAliasTestCount>();
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
EXPECT_TRUE(visitor.AreAllSpawnablesReady());
}
TEST_F(SpawnableTest, EntityAliasVisitor_AreAllSpawnablesReady_CheckFakeNotLoadedAssets_ReturnsFalse)
{
using namespace AzFramework;
InsertEntities(8);
InsertEntityAliases<8>(true);
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
EXPECT_FALSE(visitor.AreAllSpawnablesReady());
}
//
// ListTargetSpawnables
//
TEST_F(SpawnableTest, EntityAliasVisitor_ListTargetSpawnables_ListAllTargetAssets_AllTargetsListed)
{
using namespace AzFramework;
InsertEntities(DefaultEntityAliasTestCount);
InsertEntityAliases<DefaultEntityAliasTestCount>();
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
size_t count = 0;
bool correctAssets = true;
auto callback = [&count, &correctAssets](const AZ::Data::Asset<Spawnable>& 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;
InsertEntities(DefaultEntityAliasTestCount);
InsertEntityAliases<DefaultEntityAliasTestCount>();
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
size_t count = 0;
bool correctAsset = false;
auto callback = [&count, &correctAsset](const AZ::Data::Asset<Spawnable>& 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;
InsertEntities(DefaultEntityAliasTestCount);
InsertEntityAliases<DefaultEntityAliasTestCount>(true);
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
size_t count = 0;
bool correctAssets = true;
auto callback = [&count, &correctAssets](const AZ::Data::Asset<Spawnable>& 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;
InsertEntities(DefaultEntityAliasTestCount);
InsertEntityAliases<DefaultEntityAliasTestCount>(false);
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
size_t count = 0;
auto callback = [&count](const AZ::Data::Asset<Spawnable>& /*targetSpawnable*/)
{
count++;
};
visitor.ListSpawnablesRequiringLoad(callback);
EXPECT_EQ(0, count);
}
} // namespace UnitTest
@@ -10,6 +10,7 @@ set(FILES
Main.cpp
Spawnable/SpawnableEntitiesInterfaceTests.cpp
Spawnable/SpawnableEntitiesManagerTests.cpp
Spawnable/SpawnableTests.cpp
ArchiveCompressionTests.cpp
ArchiveTests.cpp
BehaviorEntityTests.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;
}
@@ -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<AZ::Entity>&& 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<AZ::Entity>&& entity, EntityAlias entityAlias)
{
return
RegisterEntity(entity->GetId(), entityAlias) &&
m_entities.emplace(AZStd::move(entityAlias), AZStd::move(entity)).second;
}
AZStd::unique_ptr<AZ::Entity> Instance::DetachEntity(const AZ::EntityId& entityId)
@@ -228,6 +231,23 @@ namespace AzToolsFramework
m_entities.clear();
}
AZStd::unique_ptr<AZ::Entity> Instance::ReplaceEntity(AZStd::unique_ptr<AZ::Entity>&& entity, EntityAliasView alias)
{
AZStd::unique_ptr<AZ::Entity> 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<bool(const AZStd::unique_ptr<AZ::Entity>&)>& filter)
{
@@ -377,7 +397,12 @@ namespace AzToolsFramework
return entityAliases;
}
void Instance::GetNestedEntityIds(const AZStd::function<bool(AZ::EntityId)>& callback)
size_t Instance::GetEntityAliasCount() const
{
return m_entities.size();
}
void Instance::GetNestedEntityIds(const AZStd::function<bool(AZ::EntityId)>& callback) const
{
GetEntityIds(callback);
@@ -387,7 +412,7 @@ namespace AzToolsFramework
}
}
void Instance::GetEntityIds(const AZStd::function<bool(AZ::EntityId)>& callback)
void Instance::GetEntityIds(const AZStd::function<bool(AZ::EntityId)>& callback) const
{
for (auto&&[entityAlias, entityId] : m_templateToInstanceEntityIdMap)
{
@@ -398,6 +423,17 @@ namespace AzToolsFramework
}
}
void Instance::GetEntityIdToAlias(const AZStd::function<bool(AZ::EntityId, EntityAliasView)>& callback) const
{
for (auto&& [entityAlias, entityId] : m_templateToInstanceEntityIdMap)
{
if (!callback(entityId, entityAlias))
{
break;
}
}
}
bool Instance::GetEntities_Impl(const AZStd::function<bool(AZStd::unique_ptr<AZ::Entity>&)>& 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*, EntityAliasView> Instance::FindInstanceAndAlias(AZ::EntityId entity)
{
auto it = m_instanceToTemplateEntityIdMap.find(entity);
if (it != m_instanceToTemplateEntityIdMap.end())
{
return m_templateToInstanceEntityIdMap[alias];
return AZStd::pair<Instance*, EntityAliasView>(this, it->second);
}
return AZ::EntityId();
else
{
for (auto&& [_, instance] : m_nestedInstances)
{
AZStd::pair<Instance*, EntityAliasView> next = instance->FindInstanceAndAlias(entity);
if (next.first != nullptr)
{
return next;
}
}
}
return AZStd::pair<Instance*, EntityAliasView>(nullptr, "");
}
AZStd::pair<const Instance*, EntityAliasView> Instance::FindInstanceAndAlias(AZ::EntityId entity) const
{
return const_cast<Instance*>(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<InstanceAlias> 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<const Instance*> 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());
@@ -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<AZ::Entity>&& entity);
bool AddEntity(AZ::Entity& entity, EntityAlias entityAlias);
bool AddEntity(AZStd::unique_ptr<AZ::Entity>&& entity, EntityAlias entityAlias);
AZStd::unique_ptr<AZ::Entity> DetachEntity(const AZ::EntityId& entityId);
void DetachEntities(const AZStd::function<void(AZStd::unique_ptr<AZ::Entity>)>& 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<AZ::Entity> ReplaceEntity(AZStd::unique_ptr<AZ::Entity>&& entity, EntityAliasView alias);
/**
* Detaches all entities in the instance hierarchy.
@@ -109,13 +118,15 @@ namespace AzToolsFramework
* @return The list of EntityAliases
*/
AZStd::vector<EntityAlias> 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<bool(AZ::EntityId)>& callback);
void GetNestedEntityIds(const AZStd::function<bool(AZ::EntityId)>& callback) const;
void GetEntityIds(const AZStd::function<bool(AZ::EntityId)>& callback);
void GetEntityIds(const AZStd::function<bool(AZ::EntityId)>& callback) const;
void GetEntityIdToAlias(const AZStd::function<bool(AZ::EntityId, EntityAliasView)>& 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<AZStd::reference_wrapper<EntityAlias>> 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<Instance*, EntityAliasView> FindInstanceAndAlias(AZ::EntityId entity);
AZStd::pair<const Instance*, EntityAliasView> 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();
@@ -13,9 +13,12 @@
#include <AzCore/Serialization/Utils.h>
#include <AzFramework/Spawnable/Spawnable.h>
#include <AzToolsFramework/Entity/EditorEntityHelpers.h>
#include <AzToolsFramework/Prefab/Instance/Instance.h>
#include <AzToolsFramework/Prefab/PrefabDomUtils.h>
#include <AzToolsFramework/Prefab/Spawnable/PrefabCatchmentProcessor.h>
#include <AzToolsFramework/Prefab/Spawnable/SpawnableUtils.h>
namespace AzToolsFramework::Prefab::PrefabConversionUtils
{
void PrefabCatchmentProcessor::Process(PrefabProcessorContext& context)
@@ -37,7 +40,7 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils
->Value("Text", SerializationFormats::Text);
serializeContext->Class<PrefabCatchmentProcessor, PrefabProcessor>()
->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<AZ::Entity> 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));
}
@@ -54,6 +54,7 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils
{
processor->Process(context);
}
context.ResolveLinks();
}
size_t PrefabConversionPipeline::CalculateProcessorFingerprint(AZ::SerializeContext* context)
{
@@ -6,12 +6,26 @@
*
*/
#include <AzCore/Interface/Interface.h>
#include <AzFramework/Spawnable/Spawnable.h>
#include <AzToolsFramework/Prefab/Instance/InstanceEntityMapperInterface.h>
#include <AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.h>
#include <AzToolsFramework/Prefab/Spawnable/SpawnableUtils.h>
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<EntityAliasPrefabLink>(&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<EntityAliasSpawnableLink>(spawnable, id);
}
auto targetPrefab = AZStd::get_if<EntityAliasPrefabLink>(&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<EntityAliasSpawnableLink>(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<AZ::Data::AssetId, AzFramework::Spawnable::EntityAliasVisitor> aliasVisitors;
for (EntityAliasStore& alias : m_entityAliases)
{
auto source = AZStd::get_if<EntityAliasSpawnableLink>(&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<EntityAliasSpawnableLink>(&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<AZ::u64>(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<AZ::u64>(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.IsValid(), "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<AzFramework::Spawnable>(&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
@@ -9,24 +9,83 @@
#pragma once
#include <AzCore/Component/ComponentExport.h>
#include <AzCore/IO/Path/Path.h>
#include <AzCore/Memory/SystemAllocator.h>
#include <AzCore/Serialization/Json/JsonSerialization.h>
#include <AzCore/std/containers/unordered_map.h>
#include <AzCore/std/containers/variant.h>
#include <AzCore/std/containers/vector.h>
#include <AzCore/std/functional.h>
#include <AzCore/std/string/string.h>
#include <AzCore/std/string/string_view.h>
#include <AzFramework/Spawnable/Spawnable.h>
#include <AzToolsFramework/Prefab/Instance/Instance.h>
#include <AzToolsFramework/Prefab/PrefabDomTypes.h>
#include <AzToolsFramework/Prefab/Spawnable/ProcesedObjectStore.h>
namespace AzToolsFramework::Prefab::PrefabConversionUtils
{
enum class EntityAliasType : uint8_t
{
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. 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. 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.
};
struct EntityAliasSpawnableLink
{
EntityAliasSpawnableLink(AzFramework::Spawnable& spawnable, AZ::EntityId index);
AzFramework::Spawnable& m_spawnable;
AZ::EntityId m_index;
};
struct EntityAliasPrefabLink
{
EntityAliasPrefabLink(AZStd::string prefabName, AzToolsFramework::Prefab::AliasPath alias);
AZStd::string m_prefabName;
AzToolsFramework::Prefab::AliasPath m_alias;
};
struct EntityAliasStore
{
using LinkStore = AZStd::variant<AZStd::monostate, EntityAliasSpawnableLink, EntityAliasPrefabLink>;
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<ProcessedObjectStore>;
using ProductAssetDependencyContainer =
AZStd::unordered_map<AZ::Data::AssetId, AZStd::unordered_set<AZ::Data::AssetId>>;
using ProductAssetDependencyContainer = AZStd::unordered_multimap<AZ::Data::AssetId, AssetDependencyInfo>;
AZ_CLASS_ALLOCATOR(PrefabProcessorContext, AZ::SystemAllocator, 0);
AZ_RTTI(PrefabProcessorContext, "{C7D77E3A-C544-486B-B774-7C82C38FE22F}");
@@ -39,11 +98,20 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils
virtual void ListPrefabs(const AZStd::function<void(AZStd::string_view, const PrefabDom&)>& 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 +122,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<AZStd::string, PrefabDom>;
using SpawnableEntityAliasStore = AZStd::vector<EntityAliasStore>;
AZ::Data::AssetLoadBehavior ToAssetLoadBehavior(EntityAliasSpawnableLoadBehavior loadBehavior) const;
NamedPrefabContainer m_prefabs;
SpawnableEntityAliasStore m_entityAliases;
ProcessedObjectStoreContainer m_products;
ProductAssetDependencyContainer m_registeredProductAssetDependencies;
@@ -11,7 +11,16 @@
namespace AzToolsFramework::Prefab::PrefabConversionUtils
{
ProcessedObjectStore::ProcessedObjectStore(AZStd::string uniqueId, AZStd::unique_ptr<AZ::Data::AssetData> 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<AZ::Data::AssetData> 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;
@@ -26,6 +26,12 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils
public:
using SerializerFunction = AZStd::function<bool(AZStd::vector<uint8_t>&, const ProcessedObjectStore&)>;
struct AssetSmartPtrDeleter
{
void operator()(AZ::Data::AssetData* asset);
};
using AssetSmartPtr = AZStd::unique_ptr<AZ::Data::AssetData, AssetSmartPtrDeleter>;
//! 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<uint8_t>& 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<AZ::Data::AssetData> ReleaseAsset();
AssetSmartPtr ReleaseAsset();
AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>>& GetReferencedAssets();
const AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>>& GetReferencedAssets() const;
const AZStd::string& GetId() const;
private:
ProcessedObjectStore(AZStd::string uniqueId, AZStd::unique_ptr<AZ::Data::AssetData> asset, SerializerFunction assetSerializer);
ProcessedObjectStore(AZStd::string uniqueId, AssetSmartPtr asset, SerializerFunction assetSerializer);
SerializerFunction m_assetSerializer;
AZStd::unique_ptr<AZ::Data::AssetData> m_asset;
AssetSmartPtr m_asset;
AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>> m_referencedAssets;
AZStd::string m_uniqueId;
};
@@ -66,7 +72,7 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils
static_assert(AZStd::is_base_of_v<AZ::Data::AssetData, T>,
"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<T>(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<T*>(&resultLeft.GetAsset());
return AZStd::make_pair<ProcessedObjectStore, T*>(AZStd::move(resultLeft), resultRight);
@@ -8,9 +8,12 @@
#include <AzToolsFramework/Prefab/Spawnable/SpawnableUtils.h>
#include <AzCore/Casting/numeric_cast.h>
#include <AzCore/Component/ComponentApplicationBus.h>
#include <AzCore/Component/Entity.h>
#include <AzCore/Component/EntityUtils.h>
#include <AzCore/Component/TransformBus.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/std/algorithm.h>
#include <AzCore/std/smart_ptr/unique_ptr.h>
#include <AzFramework/Spawnable/Spawnable.h>
@@ -20,18 +23,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 entityId, AzToolsFramework::Prefab::Instance& source)
{
AZ::Entity* result = nullptr;
source.GetEntities(
[&result, entityId](AZStd::unique_ptr<AZ::Entity>& entity)
{
if (entity->GetId() != entityId)
{
return true;
}
else
{
result = entity.get();
return false;
}
});
return result;
}
AZ::Entity* FindEntity(AZ::EntityId entityId, AzFramework::Spawnable& source)
{
uint32_t index = AzToolsFramework::Prefab::SpawnableUtils::FindEntityIndex(entityId, source);
return index != InvalidEntityIndex ? source.GetEntities()[index].get() : nullptr;
}
template<typename T>
AZStd::unique_ptr<AZ::Entity> CloneEntity(AZ::EntityId entityId, T& 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<AZ::u64>(entityId));
auto clone = AZStd::make_unique<AZ::Entity>();
static AZ::SerializeContext* sc = GetSerializeContext();
sc->CloneObjectInplace(*clone, target);
clone->SetId(AZ::Entity::MakeId());
return clone;
}
AZStd::unique_ptr<AZ::Entity> ReplaceEntityWithPlaceholder(AZ::EntityId entityId, AzToolsFramework::Prefab::Instance& source)
{
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<AZ::u64>(entityId), 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<AZ::Entity>(entityData->get().GetId(), entityData->get().GetName());
return instance->ReplaceEntity(AZStd::move(placeholder), alias);
}
AZStd::unique_ptr<AZ::Entity> ReplaceEntityWithPlaceholder(AZ::EntityId entityId, AzFramework::Spawnable& 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<AZ::u64>(entityId));
AZStd::unique_ptr<AZ::Entity> original = AZStd::move(source.GetEntities()[index]);
AZ_Assert(
original, "SpawnbleUtils were unable to locate entity with id %zu in Spawnable for replacing.",
aznumeric_cast<AZ::u64>(entityId));
source.GetEntities()[index] = AZStd::make_unique<AZ::Entity>(original->GetId(), original->GetName());
return original;
}
template<typename Source>
AZStd::pair<AZStd::unique_ptr<AZ::Entity>, AzFramework::Spawnable::EntityAliasType> ApplyAlias(
Source& source, AZ::EntityId entityId, AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasType aliasType)
{
namespace PCU = AzToolsFramework::Prefab::PrefabConversionUtils;
using ResultPair = AZStd::pair<AZStd::unique_ptr<AZ::Entity>, AzFramework::Spawnable::EntityAliasType>;
switch (aliasType)
{
case PCU::EntityAliasType::Disable:
// No need to do anything as the alias is disabled.
return ResultPair(nullptr, AzFramework::Spawnable::EntityAliasType::Disable);
case PCU::EntityAliasType::OptionalReplace:
return ResultPair(CloneEntity(entityId, source), AzFramework::Spawnable::EntityAliasType::Replace);
case PCU::EntityAliasType::Replace:
return ResultPair(ReplaceEntityWithPlaceholder(entityId, source), AzFramework::Spawnable::EntityAliasType::Replace);
case PCU::EntityAliasType::Additional:
ResultPair(AZStd::make_unique<AZ::Entity>(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<AZ::Entity>(entityId), AzFramework::Spawnable::EntityAliasType::Merge);
default:
AZ_Assert(
false, "Invalid PrefabProcessorContext::EntityAliasType type (%i) provided.", aznumeric_cast<uint64_t>(aliasType));
return ResultPair(nullptr, AzFramework::Spawnable::EntityAliasType::Disable);
}
}
}
bool CreateSpawnable(AzFramework::Spawnable& spawnable, const PrefabDom& prefabDom)
{
AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>> referencedAssets;
return CreateSpawnable(spawnable, prefabDom, referencedAssets);
}
bool CreateSpawnable(AzFramework::Spawnable& spawnable, const PrefabDom& prefabDom, AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>>& referencedAssets)
bool CreateSpawnable(
AzFramework::Spawnable& spawnable,
const PrefabDom& prefabDom,
AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>>& 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 +166,144 @@ namespace AzToolsFramework::Prefab::SpawnableUtils
}
}
AZ::Entity* CreateEntityAlias(
AZStd::string sourcePrefabName,
AzToolsFramework::Prefab::Instance& source,
AZStd::string targetPrefabName,
AzToolsFramework::Prefab::Instance& 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;
AliasPath alias = source.GetAliasPathRelativeToInstance(entityId);
if (!alias.empty())
{
auto&& [replacement, storedAliasType] = Internal::ApplyAlias(source, entityId, aliasType);
if (replacement)
{
AZ::Entity* result = replacement.get();
target.AddEntity(AZStd::move(replacement), alias.Filename().Native());
EntityAliasStore store;
store.m_aliasType = storedAliasType;
store.m_source.emplace<EntityAliasPrefabLink>(AZStd::move(sourcePrefabName), AZStd::move(alias));
store.m_target.emplace<EntityAliasPrefabLink>(
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, "A replacement for entity with id %zu could not be created.", static_cast<AZ::u64>(entityId));
return nullptr;
}
}
else
{
AZ_Assert(false, "Entity with id %zu was not found in the source prefab.", static_cast<AZ::u64>(entityId));
return nullptr;
}
}
AZ::Entity* CreateEntityAlias(
AZStd::string sourcePrefabName,
AzToolsFramework::Prefab::Instance& 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;
AliasPath alias = source.GetAliasPathRelativeToInstance(entityId);
if (!alias.empty())
{
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<EntityAliasPrefabLink>(AZStd::move(sourcePrefabName), AZStd::move(alias));
store.m_target.emplace<EntityAliasSpawnableLink>(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<AZ::u64>(entityId));
return nullptr;
}
}
else
{
AZ_Assert(false, "Entity with id %llu was not found in the source prefab.", static_cast<AZ::u64>(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<EntityAliasSpawnableLink>(source, entityId);
store.m_target.emplace<EntityAliasSpawnableLink>(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<AZ::u64>(entityId));
return nullptr;
}
}
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 aznumeric_caster(AZStd::distance(begin, it));
}
}
return InvalidEntityIndex;
}
template<typename EntityPtr>
void OrganizeEntitiesForSorting(
AZStd::vector<EntityPtr>& entities,
@@ -8,14 +8,59 @@
#pragma once
#include <AzCore/Component/EntityId.h>
#include <AzCore/std/limits.h>
#include <AzFramework/Spawnable/Spawnable.h>
#include <AzToolsFramework/Prefab/PrefabDomTypes.h>
#include <AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.h>
namespace AZ
{
class Entity;
}
namespace AzToolsFramework::Prefab
{
class Instance;
}
namespace AzToolsFramework::Prefab::SpawnableUtils
{
static constexpr uint32_t InvalidEntityIndex = AZStd::numeric_limits<uint32_t>::max();
bool CreateSpawnable(AzFramework::Spawnable& spawnable, const PrefabDom& prefabDom);
bool CreateSpawnable(AzFramework::Spawnable& spawnable, const PrefabDom& prefabDom, AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>>& referencedAssets);
AZ::Entity* CreateEntityAlias(
AZStd::string sourcePrefabName,
AzToolsFramework::Prefab::Instance& source,
AZStd::string targetPrefabName,
AzToolsFramework::Prefab::Instance& target,
AZ::EntityId entityId,
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 entityId,
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 entityId,
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 <typename EntityPtr>
@@ -175,6 +175,8 @@ namespace AZ::Prefab
const AzToolsFramework::Prefab::PrefabConversionUtils::PrefabProcessorContext::ProductAssetDependencyContainer& registeredDependencies,
AZStd::vector<AssetBuilderSDK::JobProduct>& outputProducts) const
{
using namespace AzToolsFramework::Prefab::PrefabConversionUtils;
outputProducts.reserve(store.size());
AZStd::vector<uint8_t> 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));
}