Added support for entity aliases to Spawnable.
Entity aliases can be used to have a request to spawn an entity: - spawn the original entity as normal - be disabled - redirected to another entity in another spawnable - also spawn an entity from another spawnable - add the components from an entity in another spawnable An entity alias can indicate whether or not to load the spawnable dependency. If the spawnable dependency is loaded it will be loaded asynchronously because starting blocking loads in an asset handler can lead to deadlocks once there are no more jobs available to deserialize assets. Signed-off-by: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com>
This commit is contained in:
@@ -8,10 +8,447 @@
|
||||
|
||||
#include <AzCore/RTTI/ReflectContext.h>
|
||||
#include <AzCore/Serialization/SerializeContext.h>
|
||||
#include <AzCore/std/sort.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::HasLock(const EntityAliasList* aliases) const
|
||||
{
|
||||
return aliases != nullptr;
|
||||
}
|
||||
|
||||
bool Spawnable::EntityAliasVisitorBase::HasAliases(const EntityAliasList* aliases) const
|
||||
{
|
||||
AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
|
||||
return !aliases->empty();
|
||||
}
|
||||
|
||||
bool Spawnable::EntityAliasVisitorBase::AreAllSpawnablesReady(const EntityAliasList* aliases) const
|
||||
{
|
||||
AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
|
||||
for (const EntityAlias& alias : *aliases)
|
||||
{
|
||||
if ((alias.m_aliasType != Spawnable::EntityAliasType::Original && alias.m_aliasType != Spawnable::EntityAliasType::Disabled) &&
|
||||
!alias.m_spawnable.IsReady())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
auto Spawnable::EntityAliasVisitorBase::begin(const EntityAliasList* aliases) const -> EntityAliasList::const_iterator
|
||||
{
|
||||
AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
|
||||
return aliases->cbegin();
|
||||
}
|
||||
|
||||
auto Spawnable::EntityAliasVisitorBase::end(const EntityAliasList* aliases) const -> EntityAliasList::const_iterator
|
||||
{
|
||||
AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
|
||||
return aliases->cend();
|
||||
}
|
||||
|
||||
auto Spawnable::EntityAliasVisitorBase::cbegin(const EntityAliasList* aliases) const -> EntityAliasList::const_iterator
|
||||
{
|
||||
AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
|
||||
return aliases->cbegin();
|
||||
}
|
||||
|
||||
auto Spawnable::EntityAliasVisitorBase::cend(const EntityAliasList* aliases) const -> EntityAliasList::const_iterator
|
||||
{
|
||||
AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
|
||||
return aliases->cend();
|
||||
}
|
||||
|
||||
void Spawnable::EntityAliasVisitorBase::ListTargetSpawnables(
|
||||
const EntityAliasList* aliases, const ListTargetSpawanblesCallback& callback) const
|
||||
{
|
||||
AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
|
||||
AZStd::unordered_set<AZ::Data::AssetId> spawnableIds;
|
||||
for (const Spawnable::EntityAlias& alias : *aliases)
|
||||
{
|
||||
auto it = spawnableIds.find(alias.m_spawnable.GetId());
|
||||
if (it == spawnableIds.end())
|
||||
{
|
||||
callback(alias.m_spawnable);
|
||||
spawnableIds.emplace(alias.m_spawnable.GetId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Spawnable::EntityAliasVisitorBase::ListTargetSpawnables(
|
||||
const EntityAliasList* aliases, AZ::Crc32 tag, const ListTargetSpawanblesCallback& callback) const
|
||||
{
|
||||
AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
|
||||
AZStd::unordered_set<AZ::Data::AssetId> spawnableIds;
|
||||
for (const Spawnable::EntityAlias& alias : *aliases)
|
||||
{
|
||||
if (alias.m_tag == tag)
|
||||
{
|
||||
auto it = spawnableIds.find(alias.m_spawnable.GetId());
|
||||
if (it == spawnableIds.end())
|
||||
{
|
||||
callback(alias.m_spawnable);
|
||||
spawnableIds.emplace(alias.m_spawnable.GetId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// EntityAliasVisitor
|
||||
//
|
||||
|
||||
|
||||
Spawnable::EntityAliasVisitor::EntityAliasVisitor(Spawnable& owner, EntityAliasList* entityAliasList)
|
||||
: m_owner(owner)
|
||||
, m_entityAliasList(entityAliasList)
|
||||
{
|
||||
}
|
||||
|
||||
Spawnable::EntityAliasVisitor::~EntityAliasVisitor()
|
||||
{
|
||||
if (HasLock())
|
||||
{
|
||||
Optimize();
|
||||
|
||||
AZ_Assert(
|
||||
m_owner.m_lockState == LockState::Locked, "Attempting to unlock a spawnable that's not in the locked state (%i).",
|
||||
m_owner.m_lockState.load());
|
||||
m_owner.m_lockState = LockState::Unlocked;
|
||||
}
|
||||
}
|
||||
|
||||
Spawnable::EntityAliasVisitor::EntityAliasVisitor(EntityAliasVisitor&& rhs)
|
||||
: m_owner(rhs.m_owner)
|
||||
, m_entityAliasList(rhs.m_entityAliasList)
|
||||
{
|
||||
m_dirty = rhs.m_dirty;
|
||||
|
||||
rhs.m_entityAliasList = nullptr;
|
||||
rhs.m_dirty = false;
|
||||
}
|
||||
|
||||
auto Spawnable::EntityAliasVisitor::operator=(EntityAliasVisitor&& rhs) -> EntityAliasVisitor&
|
||||
{
|
||||
if (this != &rhs)
|
||||
{
|
||||
this->~EntityAliasVisitor();
|
||||
*this = EntityAliasVisitor(rhs.m_owner, rhs.m_entityAliasList);
|
||||
m_dirty = rhs.m_dirty;
|
||||
|
||||
rhs.m_entityAliasList = nullptr;
|
||||
rhs.m_dirty = false;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool Spawnable::EntityAliasVisitor::HasLock() const
|
||||
{
|
||||
return EntityAliasVisitorBase::HasLock(m_entityAliasList);
|
||||
}
|
||||
|
||||
bool Spawnable::EntityAliasVisitor::HasAliases() const
|
||||
{
|
||||
return EntityAliasVisitorBase::HasAliases(m_entityAliasList);
|
||||
}
|
||||
|
||||
bool Spawnable::EntityAliasVisitor::AreAllSpawnablesReady() const
|
||||
{
|
||||
return EntityAliasVisitorBase::AreAllSpawnablesReady(m_entityAliasList);
|
||||
}
|
||||
|
||||
auto Spawnable::EntityAliasVisitor::begin() const -> EntityAliasList::const_iterator
|
||||
{
|
||||
return EntityAliasVisitorBase::begin(m_entityAliasList);
|
||||
}
|
||||
|
||||
auto Spawnable::EntityAliasVisitor::end() const -> EntityAliasList::const_iterator
|
||||
{
|
||||
return EntityAliasVisitorBase::end(m_entityAliasList);
|
||||
}
|
||||
|
||||
auto Spawnable::EntityAliasVisitor::cbegin() const -> EntityAliasList::const_iterator
|
||||
{
|
||||
return EntityAliasVisitorBase::cbegin(m_entityAliasList);
|
||||
}
|
||||
|
||||
auto Spawnable::EntityAliasVisitor::cend() const -> EntityAliasList::const_iterator
|
||||
{
|
||||
return EntityAliasVisitorBase::cend(m_entityAliasList);
|
||||
}
|
||||
|
||||
void Spawnable::EntityAliasVisitor::ListTargetSpawnables(const ListTargetSpawanblesCallback& callback) const
|
||||
{
|
||||
EntityAliasVisitorBase::ListTargetSpawnables(m_entityAliasList, callback);
|
||||
}
|
||||
|
||||
void Spawnable::EntityAliasVisitor::ListTargetSpawnables(AZ::Crc32 tag, const ListTargetSpawanblesCallback& callback) const
|
||||
{
|
||||
EntityAliasVisitorBase::ListTargetSpawnables(m_entityAliasList, tag, callback);
|
||||
}
|
||||
|
||||
void Spawnable::EntityAliasVisitor::AddAlias(
|
||||
AZ::Data::Asset<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::ListSpawnablesPendingLoad(const ListSpawnablesPendingLoadCallback& callback)
|
||||
{
|
||||
AZ_Assert(m_entityAliasList, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
|
||||
for (Spawnable::EntityAlias& alias : *m_entityAliasList)
|
||||
{
|
||||
if (alias.m_queueLoad &&
|
||||
alias.m_aliasType != Spawnable::EntityAliasType::Original &&
|
||||
alias.m_aliasType != Spawnable::EntityAliasType::Disabled &&
|
||||
!alias.m_spawnable.IsLoading() &&
|
||||
!alias.m_spawnable.IsReady() &&
|
||||
!alias.m_spawnable.IsError())
|
||||
{
|
||||
callback(alias.m_spawnable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Spawnable::EntityAliasVisitor::UpdateAliases(const UpdateCallback& callback)
|
||||
{
|
||||
AZ_Assert(m_entityAliasList, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
|
||||
for (Spawnable::EntityAlias& alias : *m_entityAliasList)
|
||||
{
|
||||
AZ::Data::Asset<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.
|
||||
Spawnable::EntityAlias* compare = m_entityAliasList->begin();
|
||||
Spawnable::EntityAlias* it = m_entityAliasList->begin() + 1;
|
||||
Spawnable::EntityAlias* end = m_entityAliasList->end();
|
||||
while (it < end)
|
||||
{
|
||||
switch (it->m_aliasType)
|
||||
{
|
||||
case Spawnable::EntityAliasType::Original:
|
||||
// If this is the only alias for the entity then the original can be removed.
|
||||
{
|
||||
Spawnable::EntityAlias* next = it + 1;
|
||||
if (next == end || next->m_sourceIndex != it->m_sourceIndex)
|
||||
{
|
||||
// Erase instead of a swap-and-pop in order to preserver the order.
|
||||
m_entityAliasList->erase(compare);
|
||||
--end;
|
||||
break;
|
||||
}
|
||||
}
|
||||
[[fallthrough]];
|
||||
case Spawnable::EntityAliasType::Disabled:
|
||||
[[fallthrough]];
|
||||
case Spawnable::EntityAliasType::Replace:
|
||||
// If the previous entry was a disabled, original or replace alias then remove it as it will be overwritten by the
|
||||
// current entry.
|
||||
if (compare->m_sourceIndex == it->m_sourceIndex &&
|
||||
(compare->m_aliasType == Spawnable::EntityAliasType::Original ||
|
||||
compare->m_aliasType == Spawnable::EntityAliasType::Disabled ||
|
||||
compare->m_aliasType == Spawnable::EntityAliasType::Replace))
|
||||
{
|
||||
// Erase instead of a swap-and-pop in order to preserver the order.
|
||||
m_entityAliasList->erase(compare);
|
||||
--end;
|
||||
}
|
||||
else
|
||||
{
|
||||
++compare;
|
||||
++it;
|
||||
}
|
||||
break;
|
||||
case Spawnable::EntityAliasType::Additional:
|
||||
[[fallthrough]];
|
||||
case Spawnable::EntityAliasType::Merge:
|
||||
// If this is the first entry for this type insert an original in front of it so the spawnable entity manager
|
||||
// does have to check for the case there's a merge and/or addition without a prefix.
|
||||
if (compare->m_sourceIndex != it->m_sourceIndex)
|
||||
{
|
||||
Spawnable::EntityAlias insert;
|
||||
// No load, as the asset is already loaded.
|
||||
insert.m_spawnable = AZ::Data::Asset<Spawnable>(&m_owner, AZ::Data::AssetLoadBehavior::NoLoad);
|
||||
insert.m_sourceIndex = it->m_sourceIndex;
|
||||
insert.m_targetIndex = it->m_sourceIndex; // Source index as the original entry for this slot is added.
|
||||
insert.m_aliasType = Spawnable::EntityAliasType::Original;
|
||||
m_entityAliasList->insert(compare, AZStd::move(insert));
|
||||
compare += 2;
|
||||
it += 2;
|
||||
++end;
|
||||
}
|
||||
else
|
||||
{
|
||||
++compare;
|
||||
++it;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
AZ_Assert(false, "Invalid Spawnable entity alias type found during asset loading: %i", compare->m_aliasType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Reclaim memory because after this point the aliases will not change anymore.
|
||||
m_entityAliasList->shrink_to_fit();
|
||||
m_dirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//
|
||||
// EntityAliasConstVisitor
|
||||
//
|
||||
|
||||
Spawnable::EntityAliasConstVisitor::EntityAliasConstVisitor(const Spawnable& owner, const EntityAliasList* entityAliasList)
|
||||
: m_owner(owner)
|
||||
, m_entityAliasList(entityAliasList)
|
||||
{
|
||||
}
|
||||
|
||||
Spawnable::EntityAliasConstVisitor::~EntityAliasConstVisitor()
|
||||
{
|
||||
if (HasLock())
|
||||
{
|
||||
AZ_Assert(
|
||||
m_owner.m_lockState < 0, "Attempting to unlock a read shared spawnable that was not in a read shared mode (%i).",
|
||||
m_owner.m_lockState.load());
|
||||
m_owner.m_lockState++;
|
||||
}
|
||||
}
|
||||
|
||||
bool Spawnable::EntityAliasConstVisitor::HasLock() const
|
||||
{
|
||||
return EntityAliasVisitorBase::HasLock(m_entityAliasList);
|
||||
}
|
||||
|
||||
bool Spawnable::EntityAliasConstVisitor::HasAliases() const
|
||||
{
|
||||
return EntityAliasVisitorBase::HasAliases(m_entityAliasList);
|
||||
}
|
||||
|
||||
bool Spawnable::EntityAliasConstVisitor::AreAllSpawnablesReady() const
|
||||
{
|
||||
return EntityAliasVisitorBase::AreAllSpawnablesReady(m_entityAliasList);
|
||||
}
|
||||
|
||||
auto Spawnable::EntityAliasConstVisitor::begin() const -> EntityAliasList::const_iterator
|
||||
{
|
||||
return EntityAliasVisitorBase::begin(m_entityAliasList);
|
||||
}
|
||||
|
||||
auto Spawnable::EntityAliasConstVisitor::end() const -> EntityAliasList::const_iterator
|
||||
{
|
||||
return EntityAliasVisitorBase::end(m_entityAliasList);
|
||||
}
|
||||
|
||||
auto Spawnable::EntityAliasConstVisitor::cbegin() const -> EntityAliasList::const_iterator
|
||||
{
|
||||
return EntityAliasVisitorBase::cbegin(m_entityAliasList);
|
||||
}
|
||||
|
||||
auto Spawnable::EntityAliasConstVisitor::cend() const -> EntityAliasList::const_iterator
|
||||
{
|
||||
return EntityAliasVisitorBase::cend(m_entityAliasList);
|
||||
}
|
||||
|
||||
void Spawnable::EntityAliasConstVisitor::ListTargetSpawnables(const ListTargetSpawanblesCallback& callback) const
|
||||
{
|
||||
EntityAliasVisitorBase::ListTargetSpawnables(m_entityAliasList, callback);
|
||||
}
|
||||
|
||||
void Spawnable::EntityAliasConstVisitor::ListTargetSpawnables(AZ::Crc32 tag, const ListTargetSpawanblesCallback& callback) const
|
||||
{
|
||||
EntityAliasVisitorBase::ListTargetSpawnables(m_entityAliasList, tag, callback);
|
||||
}
|
||||
|
||||
|
||||
|
||||
//
|
||||
// Spawnable
|
||||
//
|
||||
|
||||
Spawnable::Spawnable(const AZ::Data::AssetId& id, AssetStatus status)
|
||||
: AZ::Data::AssetData(id, status)
|
||||
{
|
||||
@@ -27,11 +464,56 @@ namespace AzFramework
|
||||
return m_entities;
|
||||
}
|
||||
|
||||
auto Spawnable::TryGetAliasesConst() const -> EntityAliasConstVisitor
|
||||
{
|
||||
int32_t expected = LockState::Unlocked;
|
||||
do
|
||||
{
|
||||
// Try to set the lock to a negative number to indicate a shared read.
|
||||
if (m_lockState.compare_exchange_strong(expected, expected - 1))
|
||||
{
|
||||
return EntityAliasConstVisitor(*this, &m_entityAliases);
|
||||
}
|
||||
// as long as the value is negative keep trying to get a shared read lock.
|
||||
} while (expected <= 0);
|
||||
return EntityAliasConstVisitor(*this, nullptr);
|
||||
}
|
||||
|
||||
auto Spawnable::TryGetAliases() const -> EntityAliasConstVisitor
|
||||
{
|
||||
return TryGetAliasesConst();
|
||||
}
|
||||
|
||||
auto Spawnable::TryGetAliases() -> EntityAliasVisitor
|
||||
{
|
||||
int32_t expected = LockState::Unlocked;
|
||||
return m_lockState.compare_exchange_strong(expected, LockState::Locked) ? EntityAliasVisitor(*this, &m_entityAliases)
|
||||
: EntityAliasVisitor(*this, nullptr);
|
||||
}
|
||||
|
||||
bool Spawnable::IsEmpty() const
|
||||
{
|
||||
return m_entities.empty();
|
||||
}
|
||||
|
||||
bool Spawnable::IsPermanentlyLocked() const
|
||||
{
|
||||
return m_lockState == LockState::PermanentLock;
|
||||
}
|
||||
|
||||
bool Spawnable::LockPermanently()
|
||||
{
|
||||
if (!IsPermanentlyLocked())
|
||||
{
|
||||
int32_t expected = LockState::Unlocked;
|
||||
return m_lockState.compare_exchange_strong(expected, LockState::PermanentLock);
|
||||
}
|
||||
else
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
SpawnableMetaData& Spawnable::GetMetaData()
|
||||
{
|
||||
return m_metaData;
|
||||
@@ -46,8 +528,18 @@ namespace AzFramework
|
||||
{
|
||||
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context); serializeContext != nullptr)
|
||||
{
|
||||
serializeContext->Class<Spawnable, AZ::Data::AssetData>()->Version(1)
|
||||
serializeContext->Class<Spawnable::EntityAlias>()
|
||||
->Version(1)
|
||||
->Field("Spawnable", &Spawnable::EntityAlias::m_spawnable)
|
||||
->Field("Tag", &Spawnable::EntityAlias::m_tag)
|
||||
->Field("Source Index", &Spawnable::EntityAlias::m_sourceIndex)
|
||||
->Field("Target Index", &Spawnable::EntityAlias::m_targetIndex)
|
||||
->Field("Alias Type", &Spawnable::EntityAlias::m_aliasType)
|
||||
->Field("Queue Load", &Spawnable::EntityAlias::m_queueLoad);
|
||||
|
||||
serializeContext->Class<Spawnable, AZ::Data::AssetData>()->Version(2)
|
||||
->Field("Meta data", &Spawnable::m_metaData)
|
||||
->Field("Entity aliases", &Spawnable::m_entityAliases)
|
||||
->Field("Entities", &Spawnable::m_entities);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,139 @@ namespace AzFramework
|
||||
AZ_CLASS_ALLOCATOR(Spawnable, AZ::SystemAllocator, 0);
|
||||
AZ_RTTI(AzFramework::Spawnable, "{855E3021-D305-4845-B284-20C3F7FDF16B}", AZ::Data::AssetData);
|
||||
|
||||
// The order is important for sorting in the SpawnableAssetHandler.
|
||||
enum class EntityAliasType : uint8_t
|
||||
{
|
||||
Original, //!< The original entity is spawned.
|
||||
Disabled, //!< No entity will be spawned.
|
||||
Replace, //!< The entity alias is spawned instead of the original.
|
||||
Additional, //!< The original entity is spawned as well as the alias. The alias will get a new entity id.
|
||||
Merge //!< The original entity is spawned and the components of the alias are added. The caller is responsible for
|
||||
//!< maintaining a valid component list.
|
||||
};
|
||||
|
||||
enum LockState : int32_t
|
||||
{
|
||||
Unlocked,
|
||||
Locked,
|
||||
PermanentLock
|
||||
};
|
||||
|
||||
//! An entity alias redirects the spawning of an entity to another entity, possibly in another spawnable.
|
||||
struct EntityAlias
|
||||
{
|
||||
AZ_CLASS_ALLOCATOR(EntityAlias, AZ::SystemAllocator, 0);
|
||||
AZ_TYPE_INFO(AzFramework::Spawnable::EntityAlias, "{C8D0C5BC-1F0B-4572-98C1-73B2CA8C9356}");
|
||||
|
||||
bool HasLowerIndex(const EntityAlias& other) const;
|
||||
|
||||
AZ::Data::Asset<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.
|
||||
};
|
||||
|
||||
using EntityList = AZStd::vector<AZStd::unique_ptr<AZ::Entity>>;
|
||||
using EntityAliasList = AZStd::vector<EntityAlias>;
|
||||
|
||||
private:
|
||||
class EntityAliasVisitorBase
|
||||
{
|
||||
protected:
|
||||
bool HasLock(const EntityAliasList* aliases) const;
|
||||
bool HasAliases(const EntityAliasList* aliases) const;
|
||||
bool AreAllSpawnablesReady(const EntityAliasList* aliases) const;
|
||||
|
||||
EntityAliasList::const_iterator begin(const EntityAliasList* aliases) const;
|
||||
EntityAliasList::const_iterator end(const EntityAliasList* aliases) const;
|
||||
EntityAliasList::const_iterator cbegin(const EntityAliasList* aliases) const;
|
||||
EntityAliasList::const_iterator cend(const EntityAliasList* aliases) const;
|
||||
|
||||
using ListTargetSpawanblesCallback = AZStd::function<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;
|
||||
|
||||
bool HasLock() const;
|
||||
bool HasAliases() const;
|
||||
bool AreAllSpawnablesReady() const;
|
||||
|
||||
EntityAliasList::const_iterator begin() const;
|
||||
EntityAliasList::const_iterator end() const;
|
||||
EntityAliasList::const_iterator cbegin() const;
|
||||
EntityAliasList::const_iterator cend() const;
|
||||
|
||||
void ListTargetSpawnables(const ListTargetSpawanblesCallback& callback) const;
|
||||
void ListTargetSpawnables(AZ::Crc32 tag, const ListTargetSpawanblesCallback& callback) const;
|
||||
|
||||
void AddAlias(
|
||||
AZ::Data::Asset<Spawnable> targetSpawnable,
|
||||
AZ::Crc32 tag,
|
||||
uint32_t sourceIndex,
|
||||
uint32_t targetIndex,
|
||||
Spawnable::EntityAliasType aliasType,
|
||||
bool queueLoad);
|
||||
|
||||
using ListSpawnablesPendingLoadCallback = AZStd::function<void(AZ::Data::Asset<Spawnable>& spawnablePendingLoad)>;
|
||||
void ListSpawnablesPendingLoad(const ListSpawnablesPendingLoadCallback& 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* m_entityAliasList);
|
||||
~EntityAliasConstVisitor();
|
||||
|
||||
bool HasLock() const;
|
||||
bool HasAliases() const;
|
||||
bool AreAllSpawnablesReady() const;
|
||||
|
||||
EntityAliasList::const_iterator begin() const;
|
||||
EntityAliasList::const_iterator end() const;
|
||||
EntityAliasList::const_iterator cbegin() const;
|
||||
EntityAliasList::const_iterator cend() const;
|
||||
|
||||
void ListTargetSpawnables(const ListTargetSpawanblesCallback& callback) const;
|
||||
void ListTargetSpawnables(AZ::Crc32 tag, const ListTargetSpawanblesCallback& callback) const;
|
||||
|
||||
private:
|
||||
const Spawnable& m_owner;
|
||||
const EntityAliasList* m_entityAliasList;
|
||||
|
||||
};
|
||||
|
||||
inline static constexpr const char* FileExtension = "spawnable";
|
||||
inline static constexpr const char* DotFileExtension = ".spawnable";
|
||||
@@ -39,14 +172,24 @@ namespace AzFramework
|
||||
Spawnable(const Spawnable& rhs) = delete;
|
||||
Spawnable(Spawnable&& other) = delete;
|
||||
~Spawnable() override = default;
|
||||
|
||||
|
||||
Spawnable& operator=(const Spawnable& rhs) = delete;
|
||||
Spawnable& operator=(Spawnable&& other) = delete;
|
||||
|
||||
const EntityList& GetEntities() const;
|
||||
EntityList& GetEntities();
|
||||
EntityAliasConstVisitor TryGetAliasesConst() const;
|
||||
EntityAliasConstVisitor TryGetAliases() const;
|
||||
EntityAliasVisitor TryGetAliases();
|
||||
bool IsEmpty() const;
|
||||
|
||||
//! Whether or not the spawnable is permanently locked. If so then parts of the spawnable can no longer be modified.
|
||||
bool IsPermanentlyLocked() const;
|
||||
//! Permanently locks access to parts of the spawnable from being modified.
|
||||
//! @return True if the spawnable could be locked. If false is returned another operation is still making modifications. In this case
|
||||
//! call this again at a later point in time.
|
||||
bool LockPermanently();
|
||||
|
||||
SpawnableMetaData& GetMetaData();
|
||||
const SpawnableMetaData& GetMetaData() const;
|
||||
|
||||
@@ -55,11 +198,12 @@ namespace AzFramework
|
||||
private:
|
||||
SpawnableMetaData m_metaData;
|
||||
|
||||
// Aliases that optionally replace the ones stored in this spawnable.
|
||||
EntityAliasList m_entityAliases;
|
||||
// Container for keeping all entities of the prefab the Spawnable was created from.
|
||||
// Includes both direct and nested entities of the prefab.
|
||||
EntityList m_entities;
|
||||
|
||||
mutable AZStd::atomic<int32_t> m_lockState{ LockState::Unlocked };
|
||||
};
|
||||
|
||||
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,40 @@ namespace AzFramework
|
||||
AZ::Uuid subIdHash = AZ::Uuid::CreateData(id.data(), id.size());
|
||||
return azlossy_caster(subIdHash.GetHash());
|
||||
}
|
||||
|
||||
void SpawnableAssetHandler::ResolveEntityAliases(
|
||||
Spawnable* spawnable,
|
||||
const AZ::Data::Asset<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.HasLock(), "Newly created Spawnable '%s' was already locked.", asset.GetHint().c_str());
|
||||
if (aliases.HasAliases())
|
||||
{
|
||||
AZ_Assert(
|
||||
AZStd::is_sorted(
|
||||
aliases.begin(), aliases.end(),
|
||||
[](const Spawnable::EntityAlias& lhs, const Spawnable::EntityAlias& rhs)
|
||||
{
|
||||
return lhs.HasLowerIndex(rhs);
|
||||
}),
|
||||
"Spawnable '%s' has an unsorted entity alias list.", asset.GetHint().c_str());
|
||||
|
||||
SpawnableAssetEventsBus::Broadcast(
|
||||
&SpawnableAssetEvents::OnResolveAliases, aliases, spawnable->GetMetaData(), spawnable->GetEntities());
|
||||
|
||||
aliases.Optimize();
|
||||
aliases.ListSpawnablesPendingLoad(
|
||||
[&assetLoadFilterCB, streamingDeadline, streamingPriority](AZ::Data::Asset<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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user