Merge pull request #973 from aws-lumberyard-dev/SpawnablePriorityQueue

Spawnable priority queue
This commit is contained in:
AMZN-koppersr
2021-06-01 14:09:24 -07:00
committed by GitHub
17 changed files with 543 additions and 259 deletions
@@ -21,22 +21,6 @@ namespace AzFramework
{
}
Spawnable::Spawnable(Spawnable&& other)
: m_entities(AZStd::move(other.m_entities))
{
}
Spawnable& Spawnable::operator=(Spawnable&& other)
{
if (this != &other)
{
m_entities = AZStd::move(other.m_entities);
}
return *this;
}
const Spawnable::EntityList& Spawnable::GetEntities() const
{
return m_entities;
@@ -41,11 +41,11 @@ namespace AzFramework
Spawnable() = default;
explicit Spawnable(const AZ::Data::AssetId& id, AssetStatus status = AssetStatus::NotLoaded);
Spawnable(const Spawnable& rhs) = delete;
Spawnable(Spawnable&& other);
Spawnable(Spawnable&& other) = delete;
~Spawnable() override = default;
Spawnable& operator=(const Spawnable& rhs) = delete;
Spawnable& operator=(Spawnable&& other);
Spawnable& operator=(Spawnable&& other) = delete;
const EntityList& GetEntities() const;
EntityList& GetEntities();
@@ -38,19 +38,20 @@ namespace AzFramework
void SpawnableEntitiesContainer::SpawnAllEntities()
{
AZ_Assert(m_threadData, "Calling SpawnAllEntities on a Spawnable container that's not set.");
SpawnableEntitiesInterface::Get()->SpawnAllEntities(m_threadData->m_spawnedEntitiesTicket);
SpawnableEntitiesInterface::Get()->SpawnAllEntities(m_threadData->m_spawnedEntitiesTicket, SpawnablePriority_Default);
}
void SpawnableEntitiesContainer::SpawnEntities(AZStd::vector<size_t> entityIndices)
{
AZ_Assert(m_threadData, "Calling SpawnEntities on a Spawnable container that's not set.");
SpawnableEntitiesInterface::Get()->SpawnEntities(m_threadData->m_spawnedEntitiesTicket, AZStd::move(entityIndices));
SpawnableEntitiesInterface::Get()->SpawnEntities(
m_threadData->m_spawnedEntitiesTicket, SpawnablePriority_Default, AZStd::move(entityIndices));
}
void SpawnableEntitiesContainer::DespawnAllEntities()
{
AZ_Assert(m_threadData, "Calling DespawnEntities on a Spawnable container that's not set.");
SpawnableEntitiesInterface::Get()->DespawnAllEntities(m_threadData->m_spawnedEntitiesTicket);
SpawnableEntitiesInterface::Get()->DespawnAllEntities(m_threadData->m_spawnedEntitiesTicket, SpawnablePriority_Default);
}
void SpawnableEntitiesContainer::Reset(AZ::Data::Asset<Spawnable> spawnable)
@@ -66,8 +67,10 @@ namespace AzFramework
m_monitor.Disconnect();
m_monitor.m_threadData.reset();
SpawnableEntitiesInterface::Get()->Barrier(m_threadData->m_spawnedEntitiesTicket,
[threadData = m_threadData](EntitySpawnTicket&) mutable
SpawnableEntitiesInterface::Get()->Barrier(
m_threadData->m_spawnedEntitiesTicket,
SpawnablePriority_Default,
[threadData = m_threadData](EntitySpawnTicket::Id) mutable
{
threadData.reset();
});
@@ -83,8 +86,10 @@ namespace AzFramework
void SpawnableEntitiesContainer::Alert(AlertCallback callback)
{
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&)
SpawnableEntitiesInterface::Get()->Barrier(
m_threadData->m_spawnedEntitiesTicket,
SpawnablePriority_Default,
[generation = m_threadData->m_generation, callback = AZStd::move(callback)](EntitySpawnTicket::Id)
{
callback(generation);
});
@@ -110,6 +115,7 @@ namespace AzFramework
AZ_Assert(m_threadData, "SpawnableEntitiesContainer is monitoring a spawnable, but doesn't have the associated data.");
AZ_TracePrintf("Spawnables", "Reloading spawnable '%s'.\n", replacementAsset.GetHint().c_str());
SpawnableEntitiesInterface::Get()->ReloadSpawnable(m_threadData->m_spawnedEntitiesTicket, AZStd::move(replacementAsset));
SpawnableEntitiesInterface::Get()->ReloadSpawnable(
m_threadData->m_spawnedEntitiesTicket, SpawnablePriority_Default, AZStd::move(replacementAsset));
}
} // namespace AzFramework
@@ -239,7 +239,9 @@ namespace AzFramework
{
auto manager = SpawnableEntitiesInterface::Get();
AZ_Assert(manager, "Attempting to create an entity spawn ticket while the SpawnableEntitiesInterface has no implementation.");
m_payload = manager->CreateTicket(AZStd::move(spawnable));
AZStd::pair<EntitySpawnTicket::Id, void*> result = manager->CreateTicket(AZStd::move(spawnable));
m_id = result.first;
m_payload = result.second;
}
EntitySpawnTicket::~EntitySpawnTicket()
@@ -250,6 +252,7 @@ namespace AzFramework
AZ_Assert(manager, "Attempting to destroy an entity spawn ticket while the SpawnableEntitiesInterface has no implementation.");
manager->DestroyTicket(m_payload);
m_payload = nullptr;
m_id = 0;
}
}
@@ -263,12 +266,20 @@ namespace AzFramework
AZ_Assert(manager, "Attempting to destroy an entity spawn ticket while the SpawnableEntitiesInterface has no implementation.");
manager->DestroyTicket(m_payload);
}
m_id = rhs.m_id;
rhs.m_id = 0;
m_payload = rhs.m_payload;
rhs.m_payload = nullptr;
}
return *this;
}
auto EntitySpawnTicket::GetId() const -> Id
{
return m_id;
}
bool EntitySpawnTicket::IsValid() const
{
return m_payload != nullptr;
@@ -14,6 +14,7 @@
#include <AzCore/Asset/AssetCommon.h>
#include <AzCore/Interface/Interface.h>
#include <AzCore/RTTI/TypeSafeIntegral.h>
#include <AzCore/std/functional.h>
#include <AzFramework/Spawnable/Spawnable.h>
@@ -24,6 +25,14 @@ namespace AZ
namespace AzFramework
{
AZ_TYPE_SAFE_INTEGRAL(SpawnablePriority, uint8_t);
inline static constexpr SpawnablePriority SpawnablePriority_Highest { 0 };
inline static constexpr SpawnablePriority SpawnablePriority_High { 32 };
inline static constexpr SpawnablePriority SpawnablePriority_Default { 128 };
inline static constexpr SpawnablePriority SpawnablePriority_Low { 192 };
inline static constexpr SpawnablePriority SpawnablePriority_Lowest { 255 };
class SpawnableEntityContainerView
{
public:
@@ -124,16 +133,18 @@ namespace AzFramework
SpawnableIndexEntityIterator m_end;
};
//! Requests to the SpawnableEntitiesInterface require a ticket with a valid spawnable that be used as a template. A ticket can
//! be reused for multiple calls on the same spawnable and is safe to use by multiple threads at the same time. Entities created
//! 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
//! by a call so spawn entities. The life cycle of the spawned entities is tied to the ticket and all entities spawned using a
//! by a call to spawn entities. The life cycle of the spawned entities is tied to the ticket and all entities spawned using a
//! ticket will be despawned when it's deleted.
class EntitySpawnTicket
{
public:
friend class SpawnableEntitiesDefinition;
using Id = uint64_t;
EntitySpawnTicket() = default;
EntitySpawnTicket(const EntitySpawnTicket&) = delete;
EntitySpawnTicket(EntitySpawnTicket&& rhs);
@@ -143,26 +154,37 @@ namespace AzFramework
EntitySpawnTicket& operator=(const EntitySpawnTicket&) = delete;
EntitySpawnTicket& operator=(EntitySpawnTicket&& rhs);
Id GetId() const;
bool IsValid() const;
private:
void* m_payload{ nullptr };
Id m_id { 0 }; //!< An id that uniquely identifies a ticket.
};
using EntitySpawnCallback = AZStd::function<void(EntitySpawnTicket&, SpawnableConstEntityContainerView)>;
using EntityPreInsertionCallback = AZStd::function<void(EntitySpawnTicket&, SpawnableEntityContainerView)>;
using EntityDespawnCallback = AZStd::function<void(EntitySpawnTicket&)>;
using ReloadSpawnableCallback = AZStd::function<void(EntitySpawnTicket&, SpawnableConstEntityContainerView)>;
using ListEntitiesCallback = AZStd::function<void(EntitySpawnTicket&, SpawnableConstEntityContainerView)>;
using ListIndicesEntitiesCallback = AZStd::function<void(EntitySpawnTicket&, SpawnableConstIndexEntityContainerView)>;
using ClaimEntitiesCallback = AZStd::function<void(EntitySpawnTicket&, SpawnableEntityContainerView)>;
using BarrierCallback = AZStd::function<void(EntitySpawnTicket&)>;
using EntitySpawnCallback = AZStd::function<void(EntitySpawnTicket::Id, SpawnableConstEntityContainerView)>;
using EntityPreInsertionCallback = AZStd::function<void(EntitySpawnTicket::Id, SpawnableEntityContainerView)>;
using EntityDespawnCallback = AZStd::function<void(EntitySpawnTicket::Id)>;
using ReloadSpawnableCallback = AZStd::function<void(EntitySpawnTicket::Id, SpawnableConstEntityContainerView)>;
using ListEntitiesCallback = AZStd::function<void(EntitySpawnTicket::Id, SpawnableConstEntityContainerView)>;
using ListIndicesEntitiesCallback = AZStd::function<void(EntitySpawnTicket::Id, SpawnableConstIndexEntityContainerView)>;
using ClaimEntitiesCallback = AZStd::function<void(EntitySpawnTicket::Id, SpawnableEntityContainerView)>;
using BarrierCallback = AZStd::function<void(EntitySpawnTicket::Id)>;
//! 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
//! issued from threads other than the one that issued the call, including the main thread.
//!
//! Calls on the same ticket are guaranteed to be executed in the order they are issued. Note that when issuing requests from
//! multiple threads on the same ticket the order in which the requests are assigned to the ticket is not guaranteed.
//!
//! Most calls have a priority with values that range from 0 (highest priority) to 255 (lowest priority). The implementation of this
//! interface may choose to use priority lanes which doesn't guarantee that higher priority requests happen before lower priority
//! requests if they don't pass the priority lane threshold. Priority lanes and their thresholds are implementation specific and may
//! differ between platforms. Note that if a call happened on a ticket with lower priority followed by a one with a higher priority
//! the first lower priority call will still need to complete before the second higher priority call can be executed and the priority
//! of the first call will not be updated.
class SpawnableEntitiesDefinition
{
public:
@@ -173,40 +195,48 @@ namespace AzFramework
virtual ~SpawnableEntitiesDefinition() = default;
//! Spawn instances of all entities in the spawnable.
//! @param spawnable The Spawnable asset that will be used to create entity instances from.
//! @param ticket Stores the results of the call. Use this ticket to spawn additional entities or to despawn them.
//! @param priority The priority at which this call will be executed.
//! @param completionCallback Optional callback that's called when spawning entities has completed. This can be called from
//! a different thread than the one that made the function call. The returned list of entities contains all the newly
//! created entities.
virtual void SpawnAllEntities(EntitySpawnTicket& ticket, EntityPreInsertionCallback preInsertionCallback = {},
virtual void SpawnAllEntities(
EntitySpawnTicket& ticket, SpawnablePriority priority, EntityPreInsertionCallback preInsertionCallback = {},
EntitySpawnCallback completionCallback = {}) = 0;
//! Spawn instances of some entities in the spawnable.
//! @param ticket Stores the results of the call. Use this ticket to spawn additional entities or to despawn them.
//! @param priority The priority at which this call will be executed.
//! @param entityIndices The indices into the template entities stored in the spawnable that will be used to spawn entities from.
//! @param completionCallback Optional callback that's called when spawning entities has completed. This can be called from
//! a different thread than the one that made this function call. The returned list of entities contains all the newly
//! created entities.
virtual void SpawnEntities(EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices,
virtual void SpawnEntities(
EntitySpawnTicket& ticket, SpawnablePriority priority, AZStd::vector<size_t> entityIndices,
EntityPreInsertionCallback preInsertionCallback = {}, EntitySpawnCallback completionCallback = {}) = 0;
//! Removes all entities in the provided list from the environment.
//! @param ticket The ticket previously used to spawn entities with.
//! @param priority The priority at which this call will be executed.
//! @param completionCallback Optional callback that's called when despawning entities has completed. This can be called from
//! a different thread than the one that made this function call.
virtual void DespawnAllEntities(EntitySpawnTicket& ticket, EntityDespawnCallback completionCallback = {}) = 0;
virtual void DespawnAllEntities(
EntitySpawnTicket& ticket, SpawnablePriority priority, EntityDespawnCallback completionCallback = {}) = 0;
//! Removes all entities in the provided list from the environment and reconstructs the entities from the provided spawnable.
//! @param ticket Stores the results of the call. Use this ticket to spawn additional entities or to despawn them.
//! @param ticket Holds the information on the entities to reload.
//! @param priority The priority at which this call will be executed.
//! @param spawnable The spawnable that will replace the existing spawnable. Both need to have the same asset id.
//! @param completionCallback Optional callback that's called when the entities have been reloaded. This can be called from
//! a different thread than the one that made this function call. The returned list of entities contains all the replacement
//! entities.
virtual void ReloadSpawnable(EntitySpawnTicket& ticket, AZ::Data::Asset<Spawnable> spawnable,
virtual void ReloadSpawnable(
EntitySpawnTicket& ticket, SpawnablePriority priority, AZ::Data::Asset<Spawnable> spawnable,
ReloadSpawnableCallback completionCallback = {}) = 0;
//! List all entities that are spawned using this ticket.
//! @param ticket Only the entities associated with this ticket will be listed.
//! @param priority The priority at which this call will be executed.
//! @param listCallback Required callback that will be called to list the entities on.
virtual void ListEntities(EntitySpawnTicket& ticket, ListEntitiesCallback listCallback) = 0;
virtual void ListEntities(EntitySpawnTicket& ticket, SpawnablePriority priority, ListEntitiesCallback listCallback) = 0;
//! List all entities that are spawned using this ticket with their spawnable index.
//! Spawnables contain a flat list of entities, which are used as templates to spawn entities from. For every spawned entity
//! the index of the entity in the spawnable that was used as a template is stored. This version of ListEntities will return
@@ -214,17 +244,23 @@ namespace AzFramework
//! the same index may appear multiple times as there are no restriction on how many instance of a specific entity can be
//! created.
//! @param ticket Only the entities associated with this ticket will be listed.
//! @param priority The priority at which this call will be executed.
//! @param listCallback Required callback that will be called to list the entities and indices on.
virtual void ListIndicesAndEntities(EntitySpawnTicket& ticket, ListIndicesEntitiesCallback listCallback) = 0;
virtual void ListIndicesAndEntities(
EntitySpawnTicket& ticket, SpawnablePriority priority, ListIndicesEntitiesCallback listCallback) = 0;
//! Claim all entities that are spawned using this ticket. Ownership of the entities is transferred from the ticket to the
//! caller through the callback. After this call the ticket will have no entities associated with it. The caller of
//! this function will need to manage the entities after this call.
//! @param ticket Only the entities associated with this ticket will be released.
//! @param priority The priority at which this call will be executed.
//! @param listCallback Required callback that will be called to transfer the entities through.
virtual void ClaimEntities(EntitySpawnTicket& ticket, ClaimEntitiesCallback listCallback) = 0;
virtual void ClaimEntities(EntitySpawnTicket& ticket, SpawnablePriority priority, ClaimEntitiesCallback listCallback) = 0;
//! Blocks until all operations made on the provided ticket before the barrier call have completed.
virtual void Barrier(EntitySpawnTicket& ticket, BarrierCallback completionCallback) = 0;
//! @param ticket The ticket to monitor.
//! @param priority The priority at which this call will be executed.
//! @param completionCallback Required callback that will be called as soon as the barrier has been reached.
virtual void Barrier(EntitySpawnTicket& ticket, SpawnablePriority priority, BarrierCallback completionCallback) = 0;
//! Register a handler for OnSpawned events.
virtual void AddOnSpawnedHandler(AZ::Event<AZ::Data::Asset<Spawnable>>::Handler& handler) = 0;
@@ -233,7 +269,7 @@ namespace AzFramework
virtual void AddOnDespawnedHandler(AZ::Event<AZ::Data::Asset<Spawnable>>::Handler& handler) = 0;
protected:
[[nodiscard]] virtual void* CreateTicket(AZ::Data::Asset<Spawnable>&& spawnable) = 0;
[[nodiscard]] virtual AZStd::pair<EntitySpawnTicket::Id, void*> CreateTicket(AZ::Data::Asset<Spawnable>&& spawnable) = 0;
virtual void DestroyTicket(void* ticket) = 0;
template<typename T>
@@ -10,9 +10,11 @@
*
*/
#include <AzCore/Casting/numeric_cast.h>
#include <AzCore/Component/ComponentApplicationBus.h>
#include <AzCore/Serialization/IdUtils.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/Settings/SettingsRegistry.h>
#include <AzCore/std/parallel/scoped_lock.h>
#include <AzCore/std/smart_ptr/make_shared.h>
#include <AzFramework/Components/TransformComponent.h>
@@ -22,128 +24,122 @@
namespace AzFramework
{
void SpawnableEntitiesManager::SpawnAllEntities(EntitySpawnTicket& ticket, EntityPreInsertionCallback preInsertionCallback,
template<typename T>
void SpawnableEntitiesManager::QueueRequest(EntitySpawnTicket& ticket, SpawnablePriority priority, T&& request)
{
request.m_ticket = &GetTicketPayload<Ticket>(ticket);
Queue& queue = priority <= m_highPriorityThreshold ? m_highPriorityQueue : m_regularPriorityQueue;
{
AZStd::scoped_lock queueLock(queue.m_pendingRequestMutex);
request.m_requestId = GetTicketPayload<Ticket>(ticket).m_nextRequestId++;
queue.m_pendingRequest.push(AZStd::move(request));
}
}
SpawnableEntitiesManager::SpawnableEntitiesManager()
{
if (auto settingsRegistry = AZ::SettingsRegistry::Get(); settingsRegistry != nullptr)
{
AZ::u64 value = aznumeric_caster(m_highPriorityThreshold);
settingsRegistry->Get(value, "/O3DE/AzFramework/Spawnables/HighPriorityThreshold");
m_highPriorityThreshold = aznumeric_cast<SpawnablePriority>(AZStd::clamp(value, 0llu, 255llu));
}
}
void SpawnableEntitiesManager::SpawnAllEntities(
EntitySpawnTicket& ticket, SpawnablePriority priority, EntityPreInsertionCallback preInsertionCallback,
EntitySpawnCallback completionCallback)
{
AZ_Assert(ticket.IsValid(), "Ticket provided to SpawnAllEntities hasn't been initialized.");
SpawnAllEntitiesCommand queueEntry;
queueEntry.m_ticket = &ticket;
queueEntry.m_ticketId = ticket.GetId();
queueEntry.m_completionCallback = AZStd::move(completionCallback);
queueEntry.m_preInsertionCallback = AZStd::move(preInsertionCallback);
{
AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex);
queueEntry.m_ticketId = GetTicketPayload<Ticket>(ticket).m_nextTicketId++;
m_pendingRequestQueue.push(AZStd::move(queueEntry));
}
QueueRequest(ticket, priority, AZStd::move(queueEntry));
}
void SpawnableEntitiesManager::SpawnEntities(
EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices,
EntitySpawnTicket& ticket, SpawnablePriority priority, AZStd::vector<size_t> entityIndices,
EntityPreInsertionCallback preInsertionCallback, EntitySpawnCallback completionCallback)
{
AZ_Assert(ticket.IsValid(), "Ticket provided to SpawnEntities hasn't been initialized.");
SpawnEntitiesCommand queueEntry;
queueEntry.m_ticket = &ticket;
queueEntry.m_ticketId = ticket.GetId();
queueEntry.m_entityIndices = AZStd::move(entityIndices);
queueEntry.m_completionCallback = AZStd::move(completionCallback);
queueEntry.m_preInsertionCallback = AZStd::move(preInsertionCallback);
{
AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex);
queueEntry.m_ticketId = GetTicketPayload<Ticket>(ticket).m_nextTicketId++;
m_pendingRequestQueue.push(AZStd::move(queueEntry));
}
QueueRequest(ticket, priority, AZStd::move(queueEntry));
}
void SpawnableEntitiesManager::DespawnAllEntities(EntitySpawnTicket& ticket, EntityDespawnCallback completionCallback)
void SpawnableEntitiesManager::DespawnAllEntities(
EntitySpawnTicket& ticket, SpawnablePriority priority, EntityDespawnCallback completionCallback)
{
AZ_Assert(ticket.IsValid(), "Ticket provided to DespawnAllEntities hasn't been initialized.");
DespawnAllEntitiesCommand queueEntry;
queueEntry.m_ticket = &ticket;
queueEntry.m_ticketId = ticket.GetId();
queueEntry.m_completionCallback = AZStd::move(completionCallback);
{
AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex);
queueEntry.m_ticketId = GetTicketPayload<Ticket>(ticket).m_nextTicketId++;
m_pendingRequestQueue.push(AZStd::move(queueEntry));
}
QueueRequest(ticket, priority, AZStd::move(queueEntry));
}
void SpawnableEntitiesManager::ReloadSpawnable(EntitySpawnTicket& ticket, AZ::Data::Asset<Spawnable> spawnable,
void SpawnableEntitiesManager::ReloadSpawnable(
EntitySpawnTicket& ticket, SpawnablePriority priority, AZ::Data::Asset<Spawnable> spawnable,
ReloadSpawnableCallback completionCallback)
{
AZ_Assert(ticket.IsValid(), "Ticket provided to ReloadSpawnable hasn't been initialized.");
ReloadSpawnableCommand queueEntry;
queueEntry.m_ticket = &ticket;
queueEntry.m_ticketId = ticket.GetId();
queueEntry.m_spawnable = AZStd::move(spawnable);
queueEntry.m_completionCallback = AZStd::move(completionCallback);
{
AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex);
queueEntry.m_ticketId = GetTicketPayload<Ticket>(ticket).m_nextTicketId++;
m_pendingRequestQueue.push(AZStd::move(queueEntry));
}
QueueRequest(ticket, priority, AZStd::move(queueEntry));
}
void SpawnableEntitiesManager::ListEntities(EntitySpawnTicket& ticket, ListEntitiesCallback listCallback)
void SpawnableEntitiesManager::ListEntities(EntitySpawnTicket& ticket, SpawnablePriority priority, ListEntitiesCallback listCallback)
{
AZ_Assert(listCallback, "ListEntities called on spawnable entities without a valid callback to use.");
AZ_Assert(ticket.IsValid(), "Ticket provided to ListEntities hasn't been initialized.");
ListEntitiesCommand queueEntry;
queueEntry.m_ticket = &ticket;
queueEntry.m_ticketId = ticket.GetId();
queueEntry.m_listCallback = AZStd::move(listCallback);
{
AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex);
queueEntry.m_ticketId = GetTicketPayload<Ticket>(ticket).m_nextTicketId++;
m_pendingRequestQueue.push(AZStd::move(queueEntry));
}
QueueRequest(ticket, priority, AZStd::move(queueEntry));
}
void SpawnableEntitiesManager::ListIndicesAndEntities(EntitySpawnTicket& ticket, ListIndicesEntitiesCallback listCallback)
void SpawnableEntitiesManager::ListIndicesAndEntities(
EntitySpawnTicket& ticket, SpawnablePriority priority, ListIndicesEntitiesCallback listCallback)
{
AZ_Assert(listCallback, "ListEntities called on spawnable entities without a valid callback to use.");
AZ_Assert(ticket.IsValid(), "Ticket provided to ListEntities hasn't been initialized.");
ListIndicesEntitiesCommand queueEntry;
queueEntry.m_ticket = &ticket;
queueEntry.m_ticketId = ticket.GetId();
queueEntry.m_listCallback = AZStd::move(listCallback);
{
AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex);
queueEntry.m_ticketId = GetTicketPayload<Ticket>(ticket).m_nextTicketId++;
m_pendingRequestQueue.push(AZStd::move(queueEntry));
}
QueueRequest(ticket, priority, AZStd::move(queueEntry));
}
void SpawnableEntitiesManager::ClaimEntities(EntitySpawnTicket& ticket, ClaimEntitiesCallback listCallback)
void SpawnableEntitiesManager::ClaimEntities(EntitySpawnTicket& ticket, SpawnablePriority priority, ClaimEntitiesCallback listCallback)
{
AZ_Assert(listCallback, "ClaimEntities called on spawnable entities without a valid callback to use.");
AZ_Assert(ticket.IsValid(), "Ticket provided to ClaimEntities hasn't been initialized.");
ClaimEntitiesCommand queueEntry;
queueEntry.m_ticket = &ticket;
queueEntry.m_ticketId = ticket.GetId();
queueEntry.m_listCallback = AZStd::move(listCallback);
{
AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex);
queueEntry.m_ticketId = GetTicketPayload<Ticket>(ticket).m_nextTicketId++;
m_pendingRequestQueue.push(AZStd::move(queueEntry));
}
QueueRequest(ticket, priority, AZStd::move(queueEntry));
}
void SpawnableEntitiesManager::Barrier(EntitySpawnTicket& ticket, BarrierCallback completionCallback)
void SpawnableEntitiesManager::Barrier(EntitySpawnTicket& ticket, SpawnablePriority priority, BarrierCallback completionCallback)
{
AZ_Assert(completionCallback, "Barrier on spawnable entities called without a valid callback to use.");
AZ_Assert(ticket.IsValid(), "Ticket provided to Barrier hasn't been initialized.");
BarrierCommand queueEntry;
queueEntry.m_ticket = &ticket;
queueEntry.m_ticketId = ticket.GetId();
queueEntry.m_completionCallback = AZStd::move(completionCallback);
{
AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex);
queueEntry.m_ticketId = GetTicketPayload<Ticket>(ticket).m_nextTicketId++;
m_pendingRequestQueue.push(AZStd::move(queueEntry));
}
QueueRequest(ticket, priority, AZStd::move(queueEntry));
}
void SpawnableEntitiesManager::AddOnSpawnedHandler(AZ::Event<AZ::Data::Asset<Spawnable>>::Handler& handler)
@@ -156,34 +152,54 @@ namespace AzFramework
handler.Connect(m_onDespawnedEvent);
}
auto SpawnableEntitiesManager::ProcessQueue() -> CommandQueueStatus
auto SpawnableEntitiesManager::ProcessQueue(CommandQueuePriority priority) -> CommandQueueStatus
{
CommandQueueStatus result = CommandQueueStatus::NoCommandsLeft;
if ((priority & CommandQueuePriority::High) == CommandQueuePriority::High)
{
if (ProcessQueue(m_highPriorityQueue) == CommandQueueStatus::HasCommandsLeft)
{
result = CommandQueueStatus::HasCommandsLeft;
}
}
if ((priority & CommandQueuePriority::Regular) == CommandQueuePriority::Regular)
{
if (ProcessQueue(m_regularPriorityQueue) == CommandQueueStatus::HasCommandsLeft)
{
result = CommandQueueStatus::HasCommandsLeft;
}
}
return result;
}
auto SpawnableEntitiesManager::ProcessQueue(Queue& queue) -> CommandQueueStatus
{
AZStd::queue<Requests> pendingRequestQueue;
{
AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex);
m_pendingRequestQueue.swap(pendingRequestQueue);
AZStd::scoped_lock queueLock(queue.m_pendingRequestMutex);
queue.m_pendingRequest.swap(pendingRequestQueue);
}
if (!pendingRequestQueue.empty() || !m_delayedQueue.empty())
if (!pendingRequestQueue.empty() || !queue.m_delayed.empty())
{
AZ::SerializeContext* serializeContext = nullptr;
AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationBus::Events::GetSerializeContext);
AZ_Assert(serializeContext, "Failed to retrieve serialization context.");
// Only process the requests that are currently in this queue, not the ones that could be re-added if they still can't complete.
size_t delayedSize = m_delayedQueue.size();
size_t delayedSize = queue.m_delayed.size();
for (size_t i = 0; i < delayedSize; ++i)
{
Requests& request = m_delayedQueue.front();
Requests& request = queue.m_delayed.front();
bool result = AZStd::visit([this, serializeContext](auto&& args) -> bool
{
return ProcessRequest(args, *serializeContext);
}, request);
if (!result)
{
m_delayedQueue.emplace_back(AZStd::move(request));
queue.m_delayed.emplace_back(AZStd::move(request));
}
m_delayedQueue.pop_front();
queue.m_delayed.pop_front();
}
do
@@ -197,7 +213,7 @@ namespace AzFramework
}, request);
if (!result)
{
m_delayedQueue.emplace_back(AZStd::move(request));
queue.m_delayed.emplace_back(AZStd::move(request));
}
pendingRequestQueue.pop();
}
@@ -205,20 +221,22 @@ namespace AzFramework
// Spawning entities can result in more entities being queued to spawn. Repeat spawning until the queue is
// empty to avoid a chain of entity spawning getting dragged out over multiple frames.
{
AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex);
m_pendingRequestQueue.swap(pendingRequestQueue);
AZStd::scoped_lock queueLock(queue.m_pendingRequestMutex);
queue.m_pendingRequest.swap(pendingRequestQueue);
}
} while (!pendingRequestQueue.empty());
}
return m_delayedQueue.empty() ? CommandQueueStatus::NoCommandLeft : CommandQueueStatus::HasCommandsLeft;
return queue.m_delayed.empty() ? CommandQueueStatus::NoCommandsLeft : CommandQueueStatus::HasCommandsLeft;
}
void* SpawnableEntitiesManager::CreateTicket(AZ::Data::Asset<Spawnable>&& spawnable)
AZStd::pair<uint64_t, void*> SpawnableEntitiesManager::CreateTicket(AZ::Data::Asset<Spawnable>&& spawnable)
{
static AZStd::atomic_uint64_t idCounter { 1 };
auto result = aznew Ticket();
result->m_spawnable = AZStd::move(spawnable);
return result;
return AZStd::make_pair<EntitySpawnTicket::Id, void*>(idCounter++, result);
}
void SpawnableEntitiesManager::DestroyTicket(void* ticket)
@@ -226,9 +244,9 @@ namespace AzFramework
DestroyTicketCommand queueEntry;
queueEntry.m_ticket = reinterpret_cast<Ticket*>(ticket);
{
AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex);
queueEntry.m_ticketId = reinterpret_cast<Ticket*>(ticket)->m_nextTicketId++;
m_pendingRequestQueue.push(AZStd::move(queueEntry));
AZStd::scoped_lock queueLock(m_regularPriorityQueue.m_pendingRequestMutex);
queueEntry.m_requestId = reinterpret_cast<Ticket*>(ticket)->m_nextRequestId++;
m_regularPriorityQueue.m_pendingRequest.push(AZStd::move(queueEntry));
}
}
@@ -251,8 +269,8 @@ namespace AzFramework
bool SpawnableEntitiesManager::ProcessRequest(SpawnAllEntitiesCommand& request, AZ::SerializeContext& serializeContext)
{
Ticket& ticket = GetTicketPayload<Ticket>(*request.m_ticket);
if (ticket.m_spawnable.IsReady() && request.m_ticketId == ticket.m_currentTicketId)
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;
@@ -300,7 +318,7 @@ namespace AzFramework
// 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_ticket, SpawnableEntityContainerView(
request.m_preInsertionCallback(request.m_ticketId, SpawnableEntityContainerView(
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
}
@@ -314,13 +332,13 @@ namespace AzFramework
// 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_ticket, SpawnableConstEntityContainerView(
request.m_completionCallback(request.m_ticketId, SpawnableConstEntityContainerView(
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
}
m_onSpawnedEvent.Signal(ticket.m_spawnable);
ticket.m_currentTicketId++;
ticket.m_currentRequestId++;
return true;
}
else
@@ -331,8 +349,8 @@ namespace AzFramework
bool SpawnableEntitiesManager::ProcessRequest(SpawnEntitiesCommand& request, AZ::SerializeContext& serializeContext)
{
Ticket& ticket = GetTicketPayload<Ticket>(*request.m_ticket);
if (ticket.m_spawnable.IsReady() && request.m_ticketId == ticket.m_currentTicketId)
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;
@@ -367,9 +385,7 @@ namespace AzFramework
// 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_ticket,
SpawnableEntityContainerView(
request.m_preInsertionCallback(request.m_ticketId, SpawnableEntityContainerView(
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
}
@@ -382,13 +398,13 @@ namespace AzFramework
if (request.m_completionCallback)
{
request.m_completionCallback(*request.m_ticket, SpawnableConstEntityContainerView(
request.m_completionCallback(request.m_ticketId, SpawnableConstEntityContainerView(
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
}
m_onSpawnedEvent.Signal(ticket.m_spawnable);
ticket.m_currentTicketId++;
ticket.m_currentRequestId++;
return true;
}
else
@@ -400,8 +416,8 @@ namespace AzFramework
bool SpawnableEntitiesManager::ProcessRequest(DespawnAllEntitiesCommand& request,
[[maybe_unused]] AZ::SerializeContext& serializeContext)
{
Ticket& ticket = GetTicketPayload<Ticket>(*request.m_ticket);
if (request.m_ticketId == ticket.m_currentTicketId)
Ticket& ticket = *request.m_ticket;
if (request.m_requestId == ticket.m_currentRequestId)
{
for (AZ::Entity* entity : ticket.m_spawnedEntities)
{
@@ -417,12 +433,12 @@ namespace AzFramework
if (request.m_completionCallback)
{
request.m_completionCallback(*request.m_ticket);
request.m_completionCallback(request.m_ticketId);
}
m_onDespawnedEvent.Signal(ticket.m_spawnable);
ticket.m_currentTicketId++;
ticket.m_currentRequestId++;
return true;
}
else
@@ -433,11 +449,11 @@ namespace AzFramework
bool SpawnableEntitiesManager::ProcessRequest(ReloadSpawnableCommand& request, AZ::SerializeContext& serializeContext)
{
Ticket& ticket = GetTicketPayload<Ticket>(*request.m_ticket);
Ticket& ticket = *request.m_ticket;
AZ_Assert(ticket.m_spawnable.GetId() == request.m_spawnable.GetId(),
"Spawnable is being reloaded, but the provided spawnable has a different asset id. "
"This will likely result in unexpected entities being created.");
if (ticket.m_spawnable.IsReady() && request.m_ticketId == ticket.m_currentTicketId)
if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId)
{
// Delete the original entities.
for (AZ::Entity* entity : ticket.m_spawnedEntities)
@@ -493,11 +509,11 @@ namespace AzFramework
if (request.m_completionCallback)
{
request.m_completionCallback(*request.m_ticket, SpawnableConstEntityContainerView(
request.m_completionCallback(request.m_ticketId, SpawnableConstEntityContainerView(
ticket.m_spawnedEntities.begin(), ticket.m_spawnedEntities.end()));
}
ticket.m_currentTicketId++;
ticket.m_currentRequestId++;
m_onSpawnedEvent.Signal(ticket.m_spawnable);
@@ -511,12 +527,12 @@ namespace AzFramework
bool SpawnableEntitiesManager::ProcessRequest(ListEntitiesCommand& request, [[maybe_unused]] AZ::SerializeContext& serializeContext)
{
Ticket& ticket = GetTicketPayload<Ticket>(*request.m_ticket);
if (request.m_ticketId == ticket.m_currentTicketId)
Ticket& ticket = *request.m_ticket;
if (request.m_requestId == ticket.m_currentRequestId)
{
request.m_listCallback(*request.m_ticket, SpawnableConstEntityContainerView(
request.m_listCallback(request.m_ticketId, SpawnableConstEntityContainerView(
ticket.m_spawnedEntities.begin(), ticket.m_spawnedEntities.end()));
ticket.m_currentTicketId++;
ticket.m_currentRequestId++;
return true;
}
else
@@ -527,17 +543,15 @@ namespace AzFramework
bool SpawnableEntitiesManager::ProcessRequest(ListIndicesEntitiesCommand& request, [[maybe_unused]] AZ::SerializeContext& serializeContext)
{
Ticket& ticket = GetTicketPayload<Ticket>(*request.m_ticket);
if (request.m_ticketId == ticket.m_currentTicketId)
Ticket& ticket = *request.m_ticket;
if (request.m_requestId == ticket.m_currentRequestId)
{
AZ_Assert(
ticket.m_spawnedEntities.size() == ticket.m_spawnedEntityIndices.size(),
"Entities and indices on spawnable ticket have gone out of sync.");
request.m_listCallback(
*request.m_ticket,
SpawnableConstIndexEntityContainerView(
request.m_listCallback(request.m_ticketId, SpawnableConstIndexEntityContainerView(
ticket.m_spawnedEntities.begin(), ticket.m_spawnedEntityIndices.begin(), ticket.m_spawnedEntities.size()));
ticket.m_currentTicketId++;
ticket.m_currentRequestId++;
return true;
}
else
@@ -548,16 +562,16 @@ namespace AzFramework
bool SpawnableEntitiesManager::ProcessRequest(ClaimEntitiesCommand& request, [[maybe_unused]] AZ::SerializeContext& serializeContext)
{
Ticket& ticket = GetTicketPayload<Ticket>(*request.m_ticket);
if (request.m_ticketId == ticket.m_currentTicketId)
Ticket& ticket = *request.m_ticket;
if (request.m_requestId == ticket.m_currentRequestId)
{
request.m_listCallback(*request.m_ticket, SpawnableEntityContainerView(
request.m_listCallback(request.m_ticketId, SpawnableEntityContainerView(
ticket.m_spawnedEntities.begin(), ticket.m_spawnedEntities.end()));
ticket.m_spawnedEntities.clear();
ticket.m_spawnedEntityIndices.clear();
ticket.m_currentTicketId++;
ticket.m_currentRequestId++;
return true;
}
else
@@ -568,15 +582,15 @@ namespace AzFramework
bool SpawnableEntitiesManager::ProcessRequest(BarrierCommand& request, [[maybe_unused]] AZ::SerializeContext& serializeContext)
{
Ticket& ticket = GetTicketPayload<Ticket>(*request.m_ticket);
if (request.m_ticketId == ticket.m_currentTicketId)
Ticket& ticket = *request.m_ticket;
if (request.m_requestId == ticket.m_currentRequestId)
{
if (request.m_completionCallback)
{
request.m_completionCallback(*request.m_ticket);
request.m_completionCallback(request.m_ticketId);
}
ticket.m_currentTicketId++;
ticket.m_currentRequestId++;
return true;
}
else
@@ -587,7 +601,7 @@ namespace AzFramework
bool SpawnableEntitiesManager::ProcessRequest(DestroyTicketCommand& request, [[maybe_unused]] AZ::SerializeContext& serializeContext)
{
if (request.m_ticketId == request.m_ticket->m_currentTicketId)
if (request.m_requestId == request.m_ticket->m_currentRequestId)
{
for (AZ::Entity* entity : request.m_ticket->m_spawnedEntities)
{
@@ -606,24 +620,4 @@ namespace AzFramework
return false;
}
}
bool SpawnableEntitiesManager::IsEqualTicket(const EntitySpawnTicket* lhs, const EntitySpawnTicket* rhs)
{
return GetTicketPayload<Ticket>(lhs) == GetTicketPayload<Ticket>(rhs);
}
bool SpawnableEntitiesManager::IsEqualTicket(const Ticket* lhs, const EntitySpawnTicket* rhs)
{
return lhs == GetTicketPayload<Ticket>(rhs);
}
bool SpawnableEntitiesManager::IsEqualTicket(const EntitySpawnTicket* lhs, const Ticket* rhs)
{
return GetTicketPayload<Ticket>(lhs) == rhs;
}
bool SpawnableEntitiesManager::IsEqualTicket(const Ticket* lhs, const Ticket* rhs)
{
return lhs = rhs;
}
} // namespace AzFramework
@@ -29,8 +29,6 @@ namespace AZ
namespace AzFramework
{
using EntityIdMap = AZStd::unordered_map<AZ::EntityId, AZ::EntityId>;
class SpawnableEntitiesManager
: public SpawnableEntitiesInterface::Registrar
{
@@ -38,31 +36,47 @@ namespace AzFramework
AZ_RTTI(AzFramework::SpawnableEntitiesManager, "{6E14333F-128C-464C-94CA-A63B05A5E51C}");
AZ_CLASS_ALLOCATOR(SpawnableEntitiesManager, AZ::SystemAllocator, 0);
using EntityIdMap = AZStd::unordered_map<AZ::EntityId, AZ::EntityId>;
enum class CommandQueueStatus : bool
{
HasCommandsLeft,
NoCommandLeft
NoCommandsLeft
};
enum class CommandQueuePriority
{
High = 1 << 0,
Regular = 1 << 1
};
SpawnableEntitiesManager();
~SpawnableEntitiesManager() override = default;
//
// The following functions are thread safe
//
void SpawnAllEntities(EntitySpawnTicket& ticket, EntityPreInsertionCallback preInsertionCallback = {}, EntitySpawnCallback completionCallback = {}) override;
void SpawnEntities(EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices, EntityPreInsertionCallback preInsertionCallback = {},
void SpawnAllEntities(
EntitySpawnTicket& ticket, SpawnablePriority priority, EntityPreInsertionCallback preInsertionCallback = {},
EntitySpawnCallback completionCallback = {}) override;
void DespawnAllEntities(EntitySpawnTicket& ticket, EntityDespawnCallback completionCallback = {}) override;
void SpawnEntities(
EntitySpawnTicket& ticket, SpawnablePriority priority, AZStd::vector<size_t> entityIndices,
EntityPreInsertionCallback preInsertionCallback = {},
EntitySpawnCallback completionCallback = {}) override;
void DespawnAllEntities(
EntitySpawnTicket& ticket, SpawnablePriority priority, EntityDespawnCallback completionCallback = {}) override;
void ReloadSpawnable(EntitySpawnTicket& ticket, AZ::Data::Asset<Spawnable> spawnable,
void ReloadSpawnable(
EntitySpawnTicket& ticket, SpawnablePriority priority, AZ::Data::Asset<Spawnable> spawnable,
ReloadSpawnableCallback completionCallback = {}) override;
void ListEntities(EntitySpawnTicket& ticket, ListEntitiesCallback listCallback) override;
void ListIndicesAndEntities(EntitySpawnTicket& ticket, ListIndicesEntitiesCallback listCallback) override;
void ClaimEntities(EntitySpawnTicket& ticket, ClaimEntitiesCallback listCallback) override;
void ListEntities(EntitySpawnTicket& ticket, SpawnablePriority priority, ListEntitiesCallback listCallback) override;
void ListIndicesAndEntities(
EntitySpawnTicket& ticket, SpawnablePriority priority, ListIndicesEntitiesCallback listCallback) override;
void ClaimEntities(EntitySpawnTicket& ticket, SpawnablePriority priority, ClaimEntitiesCallback listCallback) override;
void Barrier(EntitySpawnTicket& spawnInfo, BarrierCallback completionCallback) override;
void Barrier(EntitySpawnTicket& spawnInfo, SpawnablePriority priority, BarrierCallback completionCallback) override;
void AddOnSpawnedHandler(AZ::Event<AZ::Data::Asset<Spawnable>>::Handler& handler) override;
void AddOnDespawnedHandler(AZ::Event<AZ::Data::Asset<Spawnable>>::Handler& handler) override;
@@ -71,13 +85,9 @@ namespace AzFramework
// The following function is thread safe but intended to be run from the main thread.
//
CommandQueueStatus ProcessQueue();
CommandQueueStatus ProcessQueue(CommandQueuePriority priority);
protected:
void* CreateTicket(AZ::Data::Asset<Spawnable>&& spawnable) override;
void DestroyTicket(void* ticket) override;
private:
struct Ticket
{
AZ_CLASS_ALLOCATOR(Ticket, AZ::ThreadPoolAllocator, 0);
@@ -86,8 +96,8 @@ namespace AzFramework
AZStd::vector<AZ::Entity*> m_spawnedEntities;
AZStd::vector<size_t> m_spawnedEntityIndices;
AZ::Data::Asset<Spawnable> m_spawnable;
uint32_t m_nextTicketId{ 0 }; //!< Next id for this ticket.
uint32_t m_currentTicketId{ 0 }; //!< The id for the command that should be executed.
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 };
};
@@ -95,64 +105,86 @@ namespace AzFramework
{
EntitySpawnCallback m_completionCallback;
EntityPreInsertionCallback m_preInsertionCallback;
EntitySpawnTicket* m_ticket;
uint32_t m_ticketId;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct SpawnEntitiesCommand
{
AZStd::vector<size_t> m_entityIndices;
EntitySpawnCallback m_completionCallback;
EntityPreInsertionCallback m_preInsertionCallback;
EntitySpawnTicket* m_ticket;
uint32_t m_ticketId;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct DespawnAllEntitiesCommand
{
EntityDespawnCallback m_completionCallback;
EntitySpawnTicket* m_ticket;
uint32_t m_ticketId;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct ReloadSpawnableCommand
{
AZ::Data::Asset<Spawnable> m_spawnable;
ReloadSpawnableCallback m_completionCallback;
EntitySpawnTicket* m_ticket;
uint32_t m_ticketId;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct ListEntitiesCommand
{
ListEntitiesCallback m_listCallback;
EntitySpawnTicket* m_ticket;
uint32_t m_ticketId;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct ListIndicesEntitiesCommand
{
ListIndicesEntitiesCallback m_listCallback;
EntitySpawnTicket* m_ticket;
uint32_t m_ticketId;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct ClaimEntitiesCommand
{
ClaimEntitiesCallback m_listCallback;
EntitySpawnTicket* m_ticket;
uint32_t m_ticketId;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct BarrierCommand
{
BarrierCallback m_completionCallback;
EntitySpawnTicket* m_ticket;
uint32_t m_ticketId;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct DestroyTicketCommand
{
Ticket* m_ticket;
uint32_t m_ticketId;
uint32_t m_requestId;
};
using Requests = AZStd::variant<
SpawnAllEntitiesCommand, SpawnEntitiesCommand, DespawnAllEntitiesCommand, ReloadSpawnableCommand, ListEntitiesCommand,
ListIndicesEntitiesCommand, ClaimEntitiesCommand, BarrierCommand, DestroyTicketCommand>;
struct Queue
{
AZStd::deque<Requests> m_delayed; //!< Requests that were processed before, but couldn't be completed.
AZStd::queue<Requests> m_pendingRequest; //!< Requests waiting to be processed for the first time.
AZStd::mutex m_pendingRequestMutex;
};
template<typename T>
void QueueRequest(EntitySpawnTicket& ticket, SpawnablePriority priority, T&& request);
AZStd::pair<EntitySpawnTicket::Id, void*> CreateTicket(AZ::Data::Asset<Spawnable>&& spawnable) override;
void DestroyTicket(void* ticket) override;
CommandQueueStatus ProcessQueue(Queue& queue);
AZ::Entity* SpawnSingleEntity(const AZ::Entity& entityTemplate,
AZ::SerializeContext& serializeContext);
@@ -169,16 +201,18 @@ namespace AzFramework
bool ProcessRequest(BarrierCommand& request, AZ::SerializeContext& serializeContext);
bool ProcessRequest(DestroyTicketCommand& request, AZ::SerializeContext& serializeContext);
[[nodiscard]] static bool IsEqualTicket(const EntitySpawnTicket* lhs, const EntitySpawnTicket* rhs);
[[nodiscard]] static bool IsEqualTicket(const Ticket* lhs, const EntitySpawnTicket* rhs);
[[nodiscard]] static bool IsEqualTicket(const EntitySpawnTicket* lhs, const Ticket* rhs);
[[nodiscard]] static bool IsEqualTicket(const Ticket* lhs, const Ticket* rhs);
AZStd::deque<Requests> m_delayedQueue; //!< Requests that were processed before, but couldn't be completed.
AZStd::queue<Requests> m_pendingRequestQueue;
AZStd::mutex m_pendingRequestQueueMutex;
Queue m_highPriorityQueue;
Queue m_regularPriorityQueue;
AZ::Event<AZ::Data::Asset<Spawnable>> m_onSpawnedEvent;
AZ::Event<AZ::Data::Asset<Spawnable>> m_onDespawnedEvent;
//! The threshold used to determine if a request goes in the regular (if bigger than the value) or high priority queue (if smaller
//! or equal to this value). The starting value of 64 is chosen as it's between default values SpawnablePriority_High and
//! SpawnablePriority_Default which gives users a bit of room to fine tune the priorities as this value can be configured
//! through the Settings Registry under the key "/O3DE/AzFramework/Spawnables/HighPriorityThreshold".
SpawnablePriority m_highPriorityThreshold { 64 };
};
AZ_DEFINE_ENUM_BITWISE_OPERATORS(AzFramework::SpawnableEntitiesManager::CommandQueuePriority);
} // namespace AzFramework
@@ -48,10 +48,23 @@ namespace AzFramework
void SpawnableSystemComponent::OnTick(float /*deltaTime*/, AZ::ScriptTimePoint /*time*/)
{
m_entitiesManager.ProcessQueue();
m_entitiesManager.ProcessQueue(
SpawnableEntitiesManager::CommandQueuePriority::High | SpawnableEntitiesManager::CommandQueuePriority::Regular);
RootSpawnableNotificationBus::ExecuteQueuedEvents();
}
int SpawnableSystemComponent::GetTickOrder()
{
return AZ::ComponentTickBus::TICK_GAME;
}
void SpawnableSystemComponent::OnSystemTick()
{
// Handle only high priority spawning events such as those created from network. These need to happen even if the client
// doesn't have focus to avoid time-out issues for instance.
m_entitiesManager.ProcessQueue(SpawnableEntitiesManager::CommandQueuePriority::High);
}
void SpawnableSystemComponent::OnCatalogLoaded([[maybe_unused]] const char* catalogFile)
{
if (!m_catalogAvailable)
@@ -168,7 +181,8 @@ namespace AzFramework
SpawnableEntitiesManager::CommandQueueStatus queueStatus;
do
{
queueStatus = m_entitiesManager.ProcessQueue();
queueStatus = m_entitiesManager.ProcessQueue(
SpawnableEntitiesManager::CommandQueuePriority::High | SpawnableEntitiesManager::CommandQueuePriority::Regular);
} while (queueStatus == SpawnableEntitiesManager::CommandQueueStatus::HasCommandsLeft);
}
@@ -28,6 +28,7 @@ namespace AzFramework
class SpawnableSystemComponent
: public AZ::Component
, public AZ::TickBus::Handler
, public AZ::SystemTickBus::Handler
, public AssetCatalogEventBus::Handler
, public RootSpawnableInterface::Registrar
, public RootSpawnableNotificationBus::Handler
@@ -58,6 +59,13 @@ namespace AzFramework
//
void OnTick(float deltaTime, AZ::ScriptTimePoint time) override;
int GetTickOrder() override;
//
// SystemTickBus
//
void OnSystemTick() override;
//
// AssetCatalogEventBus
@@ -24,17 +24,6 @@
namespace AzToolsFramework::Prefab::SpawnableUtils
{
AzFramework::Spawnable CreateSpawnable(const PrefabDom& prefabDom)
{
AzFramework::Spawnable spawnable;
AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>> referencedAssets;
[[maybe_unused]] bool result = CreateSpawnable(spawnable, prefabDom, referencedAssets);
AZ_Assert(result,
"Failed to Load Prefab Instance from given Prefab DOM while Spawnable creation.");
return spawnable;
}
bool CreateSpawnable(AzFramework::Spawnable& spawnable, const PrefabDom& prefabDom)
{
AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>> referencedAssets;
@@ -17,7 +17,6 @@
namespace AzToolsFramework::Prefab::SpawnableUtils
{
AzFramework::Spawnable CreateSpawnable(const PrefabDom& prefabDom);
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);
@@ -34,7 +34,8 @@ namespace Benchmark
{
state.PauseTiming();
auto spawnable = ::AzToolsFramework::Prefab::SpawnableUtils::CreateSpawnable(prefabDom);
AzFramework::Spawnable spawnable;
AzToolsFramework::Prefab::SpawnableUtils::CreateSpawnable(spawnable, prefabDom);
state.ResumeTiming();
}
@@ -40,7 +40,8 @@ namespace UnitTest
//Create Spawnable
auto& prefabDom = m_prefabSystemComponent->FindTemplateDom(instance->GetTemplateId());
auto spawnable = ::AzToolsFramework::Prefab::SpawnableUtils::CreateSpawnable(prefabDom);
AzFramework::Spawnable spawnable;
AzToolsFramework::Prefab::SpawnableUtils::CreateSpawnable(spawnable, prefabDom);
EXPECT_EQ(spawnable.GetEntities().size() - 1, normalEntityCount); // 1 for container entity
const auto& spawnableEntities = spawnable.GetEntities();
@@ -84,7 +85,8 @@ namespace UnitTest
//Create Spawnable
auto& prefabDom = m_prefabSystemComponent->FindTemplateDom(thirdInstance->GetTemplateId());
auto spawnable = ::AzToolsFramework::Prefab::SpawnableUtils::CreateSpawnable(prefabDom);
AzFramework::Spawnable spawnable;
AzToolsFramework::Prefab::SpawnableUtils::CreateSpawnable(spawnable, prefabDom);
EXPECT_EQ(spawnable.GetEntities().size() - 1, normalEntityCount); // 1 for container entity
const auto& spawnableEntities = spawnable.GetEntities();
@@ -55,7 +55,11 @@ namespace UnitTest
delete m_ticket;
m_ticket = nullptr;
// One more tick on the spawnable entities manager in order to delete the ticket fully.
m_manager->ProcessQueue();
while (m_manager->ProcessQueue(
AzFramework::SpawnableEntitiesManager::CommandQueuePriority::High |
AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular) !=
AzFramework::SpawnableEntitiesManager::CommandQueueStatus::NoCommandsLeft)
;
delete m_spawnableAsset;
m_spawnableAsset = nullptr;
@@ -85,6 +89,10 @@ namespace UnitTest
TestApplication* m_application { nullptr };
};
//
// SpawnAllEntitities
//
TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_Call_AllEntitiesSpawned)
{
static constexpr size_t NumEntities = 4;
@@ -92,16 +100,72 @@ namespace UnitTest
size_t spawnedEntitiesCount = 0;
auto callback =
[&spawnedEntitiesCount](AzFramework::EntitySpawnTicket&, AzFramework::SpawnableConstEntityContainerView entities)
[&spawnedEntitiesCount](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
spawnedEntitiesCount += entities.size();
};
m_manager->SpawnAllEntities(*m_ticket, {}, AZStd::move(callback));
m_manager->ProcessQueue();
m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriority_Default, {}, AZStd::move(callback));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_EQ(NumEntities, spawnedEntitiesCount);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_DeleteTicketBeforeCall_NoCrash)
{
{
AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset);
m_manager->SpawnAllEntities(ticket, AzFramework::SpawnablePriority_Default);
}
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
//
// SpawnEntities
//
TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_DeleteTicketBeforeCall_NoCrash)
{
{
AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset);
m_manager->SpawnEntities(ticket, AzFramework::SpawnablePriority_Default, {});
}
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
//
// DespawnAllEntities
//
TEST_F(SpawnableEntitiesManagerTest, DespawnAllEntities_DeleteTicketBeforeCall_NoCrash)
{
{
AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset);
m_manager->DespawnAllEntities(ticket, AzFramework::SpawnablePriority_Default);
}
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
//
// ReloadSpawnable
//
TEST_F(SpawnableEntitiesManagerTest, ReloadSpawnable_DeleteTicketBeforeCall_NoCrash)
{
{
AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset);
m_manager->ReloadSpawnable(ticket, AzFramework::SpawnablePriority_Default, *m_spawnableAsset);
}
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
//
// ListEntitities
//
TEST_F(SpawnableEntitiesManagerTest, ListEntities_Call_AllEntitiesAreReported)
{
static constexpr size_t NumEntities = 4;
@@ -110,7 +174,7 @@ namespace UnitTest
bool allValidEntityIds = true;
size_t spawnedEntitiesCount = 0;
auto callback = [&allValidEntityIds, &spawnedEntitiesCount]
(AzFramework::EntitySpawnTicket&, AzFramework::SpawnableConstEntityContainerView entities)
(AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
for (auto&& entity : entities)
{
@@ -119,14 +183,30 @@ namespace UnitTest
spawnedEntitiesCount += entities.size();
};
m_manager->SpawnAllEntities(*m_ticket);
m_manager->ListEntities(*m_ticket, AZStd::move(callback));
m_manager->ProcessQueue();
m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriority_Default);
m_manager->ListEntities(*m_ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_TRUE(allValidEntityIds);
EXPECT_EQ(NumEntities, spawnedEntitiesCount);
}
TEST_F(SpawnableEntitiesManagerTest, ListEntities_DeleteTicketBeforeCall_NoCrash)
{
auto callback = [](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView) {};
{
AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset);
m_manager->ListEntities(ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback));
}
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
//
// ListIndicesAndEntities
//
TEST_F(SpawnableEntitiesManagerTest, ListIndicesAndEntities_Call_AllEntitiesAreReportedAndIncrementByOne)
{
static constexpr size_t NumEntities = 4;
@@ -135,7 +215,7 @@ namespace UnitTest
bool allValidEntityIds = true;
size_t spawnedEntitiesCount = 0;
auto callback = [&allValidEntityIds, &spawnedEntitiesCount]
(AzFramework::EntitySpawnTicket&, AzFramework::SpawnableConstIndexEntityContainerView entities)
(AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstIndexEntityContainerView entities)
{
for (auto&& indexEntityPair : entities)
{
@@ -148,11 +228,121 @@ namespace UnitTest
}
};
m_manager->SpawnAllEntities(*m_ticket);
m_manager->ListIndicesAndEntities(*m_ticket, AZStd::move(callback));
m_manager->ProcessQueue();
m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriority_Default);
m_manager->ListIndicesAndEntities(*m_ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_TRUE(allValidEntityIds);
EXPECT_EQ(NumEntities, spawnedEntitiesCount);
}
TEST_F(SpawnableEntitiesManagerTest, ListIndicesAndEntities_DeleteTicketBeforeCall_NoCrash)
{
auto callback = [](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstIndexEntityContainerView) {};
{
AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset);
m_manager->ListIndicesAndEntities(ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback));
}
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
//
// ClaimEntities
//
TEST_F(SpawnableEntitiesManagerTest, ClaimEntities_DeleteTicketBeforeCall_NoCrash)
{
auto callback = [](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableEntityContainerView) {};
{
AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset);
m_manager->ClaimEntities(ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback));
}
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
//
// Barrier
//
TEST_F(SpawnableEntitiesManagerTest, Barrier_DeleteTicketBeforeCall_NoCrash)
{
auto callback = [](AzFramework::EntitySpawnTicket::Id) {};
{
AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset);
m_manager->Barrier(ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback));
}
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
//
// Misc. - Priority tests
//
TEST_F(SpawnableEntitiesManagerTest, Priority_HighBeforeDefault_HigherPriorityCallHappensBeforeDefaultPriorityEvenWhenQueuedLater)
{
static constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
AzFramework::EntitySpawnTicket highPriorityTicket(*m_spawnableAsset);
size_t callCounter = 1;
size_t highPriorityCallId = 0;
size_t defaultPriorityCallId = 0;
auto highCallback = [&callCounter, &highPriorityCallId]
(AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView)
{
highPriorityCallId = callCounter++;
};
auto defaultCallback = [&callCounter, &defaultPriorityCallId]
(AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView)
{
defaultPriorityCallId = callCounter++;
};
m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriority_Default, {}, AZStd::move(defaultCallback));
m_manager->SpawnAllEntities(highPriorityTicket, AzFramework::SpawnablePriority_High, {}, AZStd::move(highCallback));
m_manager->ProcessQueue(
AzFramework::SpawnableEntitiesManager::CommandQueuePriority::High |
AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_LT(highPriorityCallId, defaultPriorityCallId);
}
TEST_F(SpawnableEntitiesManagerTest, Priority_SameTicket_DefaultPriorityCallHappensBeforeHighPriority)
{
static constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
size_t callCounter = 1;
size_t highPriorityCallId = 0;
size_t defaultPriorityCallId = 0;
auto highCallback =
[&callCounter, &highPriorityCallId](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView)
{
highPriorityCallId = callCounter++;
};
auto defaultCallback =
[&callCounter, &defaultPriorityCallId](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView)
{
defaultPriorityCallId = callCounter++;
};
m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriority_Default, {}, AZStd::move(defaultCallback));
m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriority_High, {}, AZStd::move(highCallback));
m_manager->ProcessQueue(
AzFramework::SpawnableEntitiesManager::CommandQueuePriority::High |
AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
// Run a second time as the high priority task will be pending at this point.
m_manager->ProcessQueue(
AzFramework::SpawnableEntitiesManager::CommandQueuePriority::High |
AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_LT(defaultPriorityCallId, highPriorityCallId);
}
} // namespace UnitTest
@@ -59,7 +59,7 @@ namespace Multiplayer
AZ::Transform worldTm = GetEntity()->FindComponent<AzFramework::TransformComponent>()->GetWorldTM();
auto preInsertionCallback =
[worldTm = AZStd::move(worldTm), netEntityIndex = m_netEntityIndex, spawnableAssetId = m_networkSpawnableAsset.GetId()]
(AzFramework::EntitySpawnTicket&, AzFramework::SpawnableEntityContainerView entities)
(AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableEntityContainerView entities)
{
if (entities.size() == 1)
{
@@ -81,7 +81,8 @@ namespace Multiplayer
};
m_netSpawnTicket = AzFramework::EntitySpawnTicket(m_networkSpawnableAsset);
AzFramework::SpawnableEntitiesInterface::Get()->SpawnEntities(m_netSpawnTicket, {m_netEntityIndex}, preInsertionCallback);
AzFramework::SpawnableEntitiesInterface::Get()->SpawnEntities(
m_netSpawnTicket, AzFramework::SpawnablePriority_Default, { m_netEntityIndex }, preInsertionCallback);
}
}
@@ -89,7 +90,7 @@ namespace Multiplayer
{
if(m_netSpawnTicket.IsValid())
{
AzFramework::SpawnableEntitiesInterface::Get()->DespawnAllEntities(m_netSpawnTicket);
AzFramework::SpawnableEntitiesInterface::Get()->DespawnAllEntities(m_netSpawnTicket, AzFramework::SpawnablePriority_Default);
}
}
@@ -105,7 +105,7 @@ namespace ScriptCanvas::Nodeables::Spawning
return;
}
auto preSpawnCB = [this, translation, rotation, scale]([[maybe_unused]] AzFramework::EntitySpawnTicket& ticket,
auto preSpawnCB = [this, translation, rotation, scale]([[maybe_unused]] AzFramework::EntitySpawnTicket::Id ticketId,
AzFramework::SpawnableEntityContainerView view)
{
AZ::Entity* rootEntity = *view.begin();
@@ -122,7 +122,7 @@ namespace ScriptCanvas::Nodeables::Spawning
}
};
auto spawnCompleteCB = [this]([[maybe_unused]] AzFramework::EntitySpawnTicket& ticket,
auto spawnCompleteCB = [this]([[maybe_unused]] AzFramework::EntitySpawnTicket::Id ticketId,
AzFramework::SpawnableConstEntityContainerView view)
{
AZStd::lock_guard<AZStd::recursive_mutex> lock(m_idBatchMutex);
@@ -134,6 +134,7 @@ namespace ScriptCanvas::Nodeables::Spawning
m_spawnBatchSizes.push_back(view.size());
};
AzFramework::SpawnableEntitiesInterface::Get()->SpawnAllEntities(m_spawnTicket, preSpawnCB, spawnCompleteCB);
AzFramework::SpawnableEntitiesInterface::Get()->SpawnAllEntities(
m_spawnTicket, AzFramework::SpawnablePriority_Default, preSpawnCB, spawnCompleteCB);
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"O3DE":
{
"AzFramework":
{
"Spawnables":
{
// Any requests with a priorty value equal or smaller than this will be considered a high priority request.
// The range for this value is between 0 and 255.
"HighPriorityThreshold" : 64
}
}
}
}