Integrating up through commit 90f050496

This commit is contained in:
alexpete
2021-04-07 14:03:29 -07:00
parent 8f2ed080a9
commit c2cbd430fe
2694 changed files with 285622 additions and 176874 deletions
@@ -0,0 +1,71 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzCore/Asset/AssetCommon.h>
#include <AzCore/EBus/EBus.h>
#include <AzCore/Interface/Interface.h>
#include <AzCore/RTTI/RTTI.h>
#include <AzFramework/Spawnable/Spawnable.h>
namespace AzFramework
{
//! Notifications send when the root spawnable updates. Events will always be called from the main thread.
class RootSpawnableNotifications
: public AZ::EBusTraits
{
public:
virtual ~RootSpawnableNotifications() = default;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple;
static const bool EnableEventQueue = true;
using MutexType = AZStd::recursive_mutex;
//! Called when the root spawnable has been assigned a new value. This may be called several times without a call to release
//! in between.
//! @param rootSpawnable The new root spawnable that was assigned.
//! @param generation The generation of the root spawnable. This will increment every time a new spawnable is assigned.
virtual void OnRootSpawnableAssigned([[maybe_unused]] AZ::Data::Asset<Spawnable> rootSpawnable,
[[maybe_unused]] uint32_t generation) {}
//! Called when the root spawnable has Released. This will only be called if there's no root spawnable assigned to take the
//! place of the original root spawnable.
//! @param generation The generation of the root spawnable that was released.
virtual void OnRootSpawnableReleased([[maybe_unused]] uint32_t generation) {}
};
using RootSpawnableNotificationBus = AZ::EBus<RootSpawnableNotifications>;
//! Interface to manage the root spawnable. All calls to this interface need to be made
//! from the main thread and all events are called from the main thread.
class RootSpawnableDefinition
{
public:
AZ_RTTI(AzFramework::RootSpawnableDefinition, "{6F3698F4-005D-4F26-BE99-5DC2E21FAD38}");
using OnRootSpawnableReadyEvent = AZ::Event < const AZ::Data::Asset<Spawnable>&, bool>;
//! Sets the provided spawnable as the new root spawnable. If a root spawnable has already
//! been assigned this will unload any entities spawned from it and replace it. The provided
//! spawnable will become the new root and all entities in it will be instanced into the
//! game entity context.
//! @return the generation of the root spawnable that has been assigned.
virtual uint64_t AssignRootSpawnable(AZ::Data::Asset<Spawnable> rootSpawnable) = 0;
//! Releases the root spawnable if one is set, resulting in all entities spawned from it to
//! be deleted and the spawnable asset to be released. This call is automatically done when
//! AssignRootSpawnable is called while a root spawnable is assigned.
virtual void ReleaseRootSpawnable() = 0;
};
using RootSpawnableInterface = AZ::Interface<RootSpawnableDefinition>;
} // namespace AzFramework
@@ -16,8 +16,8 @@
namespace AzFramework
{
Spawnable::Spawnable(const AZ::Data::AssetId& id)
: AZ::Data::AssetData(id)
Spawnable::Spawnable(const AZ::Data::AssetId& id, AssetStatus status)
: AZ::Data::AssetData(id, status)
{
}
@@ -33,9 +33,10 @@ namespace AzFramework
using EntityList = AZStd::vector<AZStd::unique_ptr<AZ::Entity>>;
inline static constexpr const char* FileExtension = "spawnable";
inline static constexpr const char* DotFileExtension = ".spawnable";
Spawnable() = default;
explicit Spawnable(const AZ::Data::AssetId& id);
explicit Spawnable(const AZ::Data::AssetId& id, AssetStatus status = AssetStatus::NotLoaded);
Spawnable(const Spawnable& rhs) = delete;
Spawnable(Spawnable&& other);
~Spawnable() override = default;
@@ -12,8 +12,8 @@
#include <AzCore/Serialization/Utils.h>
#include <AzCore/std/string/string.h>
#include <AzFramework/Spawnable/SpawnableAssetHandler.h>
#include <AzFramework/Spawnable/Spawnable.h>
#include <AzFramework/Spawnable/SpawnableAssetHandler.h>
namespace AzFramework
{
@@ -0,0 +1,115 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzCore/std/smart_ptr/make_shared.h>
#include <AzFramework/Spawnable/SpawnableEntitiesContainer.h>
namespace AzFramework
{
SpawnableEntitiesContainer::SpawnableEntitiesContainer(AZ::Data::Asset<Spawnable> spawnable)
{
Connect(AZStd::move(spawnable));
}
SpawnableEntitiesContainer::~SpawnableEntitiesContainer()
{
Clear();
}
bool SpawnableEntitiesContainer::IsSet() const
{
return m_threadData != nullptr;
}
uint64_t SpawnableEntitiesContainer::GetCurrentGeneration() const
{
return m_currentGeneration;
}
void SpawnableEntitiesContainer::SpawnAllEntities()
{
AZ_Assert(m_threadData, "Calling SpawnAllEntities on a Spawnable container that's not set.");
SpawnableEntitiesInterface::Get()->SpawnAllEntities(m_threadData->m_spawnedEntitiesTicket);
}
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));
}
void SpawnableEntitiesContainer::DespawnAllEntities()
{
AZ_Assert(m_threadData, "Calling DespawnEntities on a Spawnable container that's not set.");
SpawnableEntitiesInterface::Get()->DespawnAllEntities(m_threadData->m_spawnedEntitiesTicket);
}
void SpawnableEntitiesContainer::Reset(AZ::Data::Asset<Spawnable> spawnable)
{
Clear();
Connect(AZStd::move(spawnable));
}
void SpawnableEntitiesContainer::Clear()
{
if (m_threadData != nullptr)
{
m_monitor.Disconnect();
m_monitor.m_threadData.reset();
SpawnableEntitiesInterface::Get()->Barrier(m_threadData->m_spawnedEntitiesTicket,
[threadData = m_threadData](EntitySpawnTicket&) mutable
{
threadData.reset();
});
m_threadData.reset();
// The generation is incremented here instead of Connect in order to make sure that any callback that checks the
// provided generation with the current generation is aware that the container has moved on to the next iteration
// of the container even though it's empty and unassigned at this point.
m_currentGeneration++;
}
}
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&)
{
callback(generation);
});
}
void SpawnableEntitiesContainer::Connect(AZ::Data::Asset<Spawnable> spawnable)
{
AZ::Data::AssetId spawnableId = spawnable.GetId();
AZ_Assert(m_threadData == nullptr, "Connecting a spawnable entities container that's already connected.");
AZ_Assert(m_monitor.m_threadData == nullptr, "Connecting a spawnable entities monitor that's already connected.");
m_threadData = AZStd::make_shared<ThreadSafeData>();
m_threadData->m_spawnedEntitiesTicket = EntitySpawnTicket(AZStd::move(spawnable));
m_threadData->m_generation = m_currentGeneration;
m_monitor.m_threadData = m_threadData;
m_monitor.Connect(spawnableId);
}
void SpawnableEntitiesContainer::Monitor::OnSpawnableReloaded(AZ::Data::Asset<Spawnable>&& replacementAsset)
{
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));
}
} // namespace AzFramework
@@ -0,0 +1,105 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzCore/Asset/AssetCommon.h>
#include <AzCore/EBus/Event.h>
#include <AzCore/Memory/Memory.h>
#include <AzCore/std/functional.h>
#include <AzCore/std/containers/vector.h>
#include <AzCore/std/parallel/atomic.h>
#include <AzCore/std/smart_ptr/shared_ptr.h>
#include <AzFramework/Spawnable/Spawnable.h>
#include <AzFramework/Spawnable/SpawnableEntitiesInterface.h>
#include <AzFramework/Spawnable/SpawnableMonitor.h>
namespace AZ
{
class Entity;
}
namespace AzFramework
{
//! A utility class to simplify the life-cycle management of entities created from a Spawnable.
//! This class will keep track of the created entities and take appropriate action when the spawnable changes.
//! Calls to this container should be done from the same thread, but multiple threads can create their own container for
//! the same Spawnable. This container is for simple use cases. Complex situations can directly call the
//! SpawnablesEntitesInterface and use the SpawnableMonitor if needed.
class SpawnableEntitiesContainer
{
public:
using AlertCallback = AZStd::function<void(uint32_t generation)>;
//! Constructs a new spawnables entity container that has not been connected.
SpawnableEntitiesContainer() = default;
//! Constructs a new spawnables entity container that connects to the provided spawnable.
//! @param spawnable The Spawnable that will be monitored and used as a template to create entities from.
explicit SpawnableEntitiesContainer(AZ::Data::Asset<Spawnable> spawnable);
~SpawnableEntitiesContainer();
//! Returns true if the container has a spawnable set and can process requests, otherwise returns falls.
[[nodiscard]] bool IsSet() const;
//! Returns a number that identifies the current generation of the container with. The completion callback can still receive
//! calls from older generations as processing completes on those. The returned value can be used to help calls tell
//! older versions apart from newer ones.
[[nodiscard]] uint64_t GetCurrentGeneration() const;
//! Puts in a request to spawn entities using all entities in the provided spawnable as a template.
void SpawnAllEntities();
//! Puts in a request to spawn entities using the entities found in the spawnable at the provided indices as a template.
//! @param entityIndices A list of indices to the entities in the spawnable.
void SpawnEntities(AZStd::vector<size_t> entityIndices);
//! Puts in a request to despawn all previous spawned entities.
void DespawnAllEntities();
//! Resets the spawnable and completion callback. This call will clear first if a spawnable has already been set. See
//! Clear for more details.
//! @param spawnable The Spawnable that will be monitored and used as a template to create entities from.
void Reset(AZ::Data::Asset<Spawnable> spawnable);
//! Puts in a request to disconnect from the connected spawnable. This will immediately clear the internal
//! state to allow for a Reset, but the release of the spawnable itself will be delayed. As a result any pending and
//! in-flight requests will complete first. If a callback has been set it will be called one more time after this
//! function returns, possibly outliving the lifetime of the container.
void Clear();
//! Adds an alert that will trigger the provided callback once all previous calls to change the container have completed.
//! This includes calls to (de)spawn entities, reset the container or clear. The callback can be called from threads
//! other than the calling thread including the main thread. Note that because the alert is queued it can still be called
//! after the container has been deleted or can be called for a previously assigned spawnable. In the latter case check
//! if the current generation matches the generation provided with the callback.
void Alert(AlertCallback callback);
private:
void Connect(AZ::Data::Asset<Spawnable> spawnable);
struct ThreadSafeData
{
AZ_CLASS_ALLOCATOR(SpawnableEntitiesContainer::ThreadSafeData, AZ::SystemAllocator, 0);
EntitySpawnTicket m_spawnedEntitiesTicket;
uint32_t m_generation{ 0 };
};
class Monitor final : public SpawnableMonitor
{
public:
AZStd::shared_ptr<ThreadSafeData> m_threadData;
protected:
void OnSpawnableReloaded(AZ::Data::Asset<Spawnable>&& replacementAsset) override;
};
Monitor m_monitor;
AZStd::atomic_uint32_t m_currentGeneration{ 1 };
AZStd::shared_ptr<ThreadSafeData> m_threadData;
};
} // namespace AzFramework
@@ -0,0 +1,139 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzFramework/Spawnable/SpawnableEntitiesInterface.h>
namespace AzFramework
{
SpawnableEntityContainerView::SpawnableEntityContainerView(AZ::Entity** begin, size_t length)
: m_begin(begin)
, m_end(begin + length)
{}
SpawnableEntityContainerView::SpawnableEntityContainerView(AZ::Entity** begin, AZ::Entity** end)
: m_begin(begin)
, m_end(end)
{
AZ_Assert(m_begin <= m_end, "SpawnableEntityContainerView created with a begin that's past the end.");
}
AZ::Entity** SpawnableEntityContainerView::begin()
{
return m_begin;
}
AZ::Entity** SpawnableEntityContainerView::end()
{
return m_end;
}
const AZ::Entity* const* SpawnableEntityContainerView::cbegin()
{
return m_begin;
}
const AZ::Entity* const* SpawnableEntityContainerView::cend()
{
return m_end;
}
size_t SpawnableEntityContainerView::size()
{
return AZStd::distance(m_begin, m_end);
}
SpawnableConstEntityContainerView::SpawnableConstEntityContainerView(AZ::Entity** begin, size_t length)
: m_begin(begin)
, m_end(begin + length)
{}
SpawnableConstEntityContainerView::SpawnableConstEntityContainerView(AZ::Entity** begin, AZ::Entity** end)
: m_begin(begin)
, m_end(end)
{
AZ_Assert(m_begin <= m_end, "SpawnableConstEntityContainerView created with a begin that's past the end.");
}
const AZ::Entity* const* SpawnableConstEntityContainerView::begin()
{
return m_begin;
}
const AZ::Entity* const* SpawnableConstEntityContainerView::end()
{
return m_end;
}
const AZ::Entity* const* SpawnableConstEntityContainerView::cbegin()
{
return m_begin;
}
const AZ::Entity* const* SpawnableConstEntityContainerView::cend()
{
return m_end;
}
size_t SpawnableConstEntityContainerView::size()
{
return AZStd::distance(m_begin, m_end);
}
EntitySpawnTicket::EntitySpawnTicket(EntitySpawnTicket&& rhs)
: m_payload(rhs.m_payload)
{
rhs.m_payload = nullptr;
}
EntitySpawnTicket::EntitySpawnTicket(AZ::Data::Asset<Spawnable> spawnable)
{
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));
}
EntitySpawnTicket::~EntitySpawnTicket()
{
if (m_payload)
{
auto manager = SpawnableEntitiesInterface::Get();
AZ_Assert(manager, "Attempting to destroy an entity spawn ticket while the SpawnableEntitiesInterface has no implementation.");
manager->DestroyTicket(m_payload);
m_payload = nullptr;
}
}
EntitySpawnTicket& EntitySpawnTicket::operator=(EntitySpawnTicket&& rhs)
{
if (this != &rhs)
{
if (m_payload)
{
auto manager = SpawnableEntitiesInterface::Get();
AZ_Assert(manager, "Attempting to destroy an entity spawn ticket while the SpawnableEntitiesInterface has no implementation.");
manager->DestroyTicket(m_payload);
}
m_payload = rhs.m_payload;
rhs.m_payload = nullptr;
}
return *this;
}
bool EntitySpawnTicket::IsValid() const
{
return m_payload != nullptr;
}
} // namespace AzFramework
@@ -0,0 +1,181 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzCore/Asset/AssetCommon.h>
#include <AzCore/Interface/Interface.h>
#include <AzCore/std/functional.h>
#include <AzFramework/Spawnable/Spawnable.h>
namespace AZ
{
class Entity;
}
namespace AzFramework
{
class SpawnableEntityContainerView
{
public:
SpawnableEntityContainerView(AZ::Entity** begin, size_t length);
SpawnableEntityContainerView(AZ::Entity** begin, AZ::Entity** end);
AZ::Entity** begin();
AZ::Entity** end();
const AZ::Entity* const* cbegin();
const AZ::Entity* const* cend();
size_t size();
private:
AZ::Entity** m_begin;
AZ::Entity** m_end;
};
class SpawnableConstEntityContainerView
{
public:
SpawnableConstEntityContainerView(AZ::Entity** begin, size_t length);
SpawnableConstEntityContainerView(AZ::Entity** begin, AZ::Entity** end);
const AZ::Entity* const* begin();
const AZ::Entity* const* end();
const AZ::Entity* const* cbegin();
const AZ::Entity* const* cend();
size_t size();
private:
AZ::Entity** m_begin;
AZ::Entity** 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
//! 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
//! ticket will be despawned when it's deleted.
class EntitySpawnTicket
{
public:
friend class SpawnableEntitiesDefinition;
EntitySpawnTicket() = default;
EntitySpawnTicket(const EntitySpawnTicket&) = delete;
EntitySpawnTicket(EntitySpawnTicket&& rhs);
explicit EntitySpawnTicket(AZ::Data::Asset<Spawnable> spawnable);
~EntitySpawnTicket();
EntitySpawnTicket& operator=(const EntitySpawnTicket&) = delete;
EntitySpawnTicket& operator=(EntitySpawnTicket&& rhs);
bool IsValid() const;
private:
void* m_payload{ nullptr };
};
using EntitySpawnCallback = AZStd::function<void(EntitySpawnTicket&, SpawnableConstEntityContainerView)>;
using EntityDespawnCallback = AZStd::function<void(EntitySpawnTicket&)>;
using ReloadSpawnableCallback = AZStd::function<void(EntitySpawnTicket&, SpawnableConstEntityContainerView)>;
using ListEntitiesCallback = AZStd::function<void(EntitySpawnTicket&, SpawnableConstEntityContainerView)>;
using ClaimEntitiesCallback = AZStd::function<void(EntitySpawnTicket&, SpawnableEntityContainerView)>;
using BarrierCallback = AZStd::function<void(EntitySpawnTicket&)>;
//! 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.
class SpawnableEntitiesDefinition
{
public:
AZ_RTTI(AzFramework::SpawnableEntitiesDefinition, "{A9ED3F1F-4D69-4182-B0CD-EB561EEA7068}");
friend class EntitySpawnTicket;
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 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, 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 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,
EntitySpawnCallback completionCallback = {}) = 0;
//! Removes all entities in the provided list from the environment.
//! @param ticket The ticket previously used to spawn entities with.
//! @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;
//! 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 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,
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 listCallback Required callback that will be called to list the entities on.
virtual void ListEntities(EntitySpawnTicket& ticket, ListEntitiesCallback 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 listCallback Required callback that will be called to transfer the entities through.
virtual void ClaimEntities(EntitySpawnTicket& ticket, 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;
protected:
[[nodiscard]] virtual void* CreateTicket(AZ::Data::Asset<Spawnable>&& spawnable) = 0;
virtual void DestroyTicket(void* ticket) = 0;
template<typename T>
static T& GetTicketPayload(EntitySpawnTicket& ticket)
{
return *reinterpret_cast<T*>(ticket.m_payload);
}
template<typename T>
static const T& GetTicketPayload(const EntitySpawnTicket& ticket)
{
return *reinterpret_cast<const T*>(ticket.m_payload);
}
template<typename T>
static T* GetTicketPayload(EntitySpawnTicket* ticket)
{
return reinterpret_cast<T*>(ticket->m_payload);
}
template<typename T>
static const T* GetTicketPayload(const EntitySpawnTicket* ticket)
{
return reinterpret_cast<const T*>(ticket->m_payload);
}
};
using SpawnableEntitiesInterface = AZ::Interface<SpawnableEntitiesDefinition>;
} // namespace AzFramework
@@ -0,0 +1,457 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzCore/Component/ComponentApplicationBus.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/std/parallel/scoped_lock.h>
#include <AzCore/std/smart_ptr/make_shared.h>
#include <AzFramework/Entity/GameEntityContextBus.h>
#include <AzFramework/Spawnable/Spawnable.h>
#include <AzFramework/Spawnable/SpawnableEntitiesManager.h>
namespace AzFramework
{
void SpawnableEntitiesManager::SpawnAllEntities(EntitySpawnTicket& ticket, EntitySpawnCallback completionCallback)
{
SpawnAllEntitiesCommand queueEntry;
queueEntry.m_ticket = &ticket;
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));
}
}
void SpawnableEntitiesManager::SpawnEntities(EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices,
EntitySpawnCallback completionCallback)
{
SpawnEntitiesCommand queueEntry;
queueEntry.m_ticket = &ticket;
queueEntry.m_entityIndices = AZStd::move(entityIndices);
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));
}
}
void SpawnableEntitiesManager::DespawnAllEntities(EntitySpawnTicket& ticket, EntityDespawnCallback completionCallback)
{
DespawnAllEntitiesCommand queueEntry;
queueEntry.m_ticket = &ticket;
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));
}
}
void SpawnableEntitiesManager::ReloadSpawnable(EntitySpawnTicket& ticket, AZ::Data::Asset<Spawnable> spawnable,
ReloadSpawnableCallback completionCallback)
{
ReloadSpawnableCommand queueEntry;
queueEntry.m_ticket = &ticket;
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));
}
}
void SpawnableEntitiesManager::ListEntities(EntitySpawnTicket& ticket, ListEntitiesCallback listCallback)
{
AZ_Assert(listCallback, "ListEntities called on spawnable entities without a valid callback to use.");
ListEntitiesCommand queueEntry;
queueEntry.m_ticket = &ticket;
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));
}
}
void SpawnableEntitiesManager::ClaimEntities(EntitySpawnTicket& ticket, ClaimEntitiesCallback listCallback)
{
AZ_Assert(listCallback, "ClaimEntities called on spawnable entities without a valid callback to use.");
ClaimEntitiesCommand queueEntry;
queueEntry.m_ticket = &ticket;
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));
}
}
void SpawnableEntitiesManager::Barrier(EntitySpawnTicket& ticket, BarrierCallback completionCallback)
{
AZ_Assert(completionCallback, "Barrier on spawnable entities called without a valid callback to use.");
BarrierCommand queueEntry;
queueEntry.m_ticket = &ticket;
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));
}
}
auto SpawnableEntitiesManager::ProcessQueue() -> CommandQueueStatus
{
AZStd::queue<Requests> pendingRequestQueue;
{
AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex);
m_pendingRequestQueue.swap(pendingRequestQueue);
}
if (!pendingRequestQueue.empty() || !m_delayedQueue.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();
for (size_t i = 0; i < delayedSize; ++i)
{
Requests& request = m_delayedQueue.front();
bool result = AZStd::visit([this, serializeContext](auto&& args) -> bool
{
return ProcessRequest(args, *serializeContext);
}, request);
if (!result)
{
m_delayedQueue.emplace_back(AZStd::move(request));
}
m_delayedQueue.pop_front();
}
do
{
while (!pendingRequestQueue.empty())
{
Requests& request = pendingRequestQueue.front();
bool result = AZStd::visit([this, serializeContext](auto&& args) -> bool
{
return ProcessRequest(args, *serializeContext);
}, request);
if (!result)
{
m_delayedQueue.emplace_back(AZStd::move(request));
}
pendingRequestQueue.pop();
}
// 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);
}
} while (!pendingRequestQueue.empty());
}
return m_delayedQueue.empty() ? CommandQueueStatus::NoCommandLeft : CommandQueueStatus::HasCommandsLeft;
}
void* SpawnableEntitiesManager::CreateTicket(AZ::Data::Asset<Spawnable>&& spawnable)
{
auto result = aznew Ticket();
result->m_spawnable = AZStd::move(spawnable);
return result;
}
void SpawnableEntitiesManager::DestroyTicket(void* ticket)
{
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));
}
}
AZ::Entity* SpawnableEntitiesManager::SpawnSingleEntity(const AZ::Entity& entityTemplate, AZ::SerializeContext& serializeContext)
{
AZ::Entity* clone = serializeContext.CloneObject(&entityTemplate);
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
clone->SetId(AZ::Entity::MakeId());
GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, clone);
return clone;
}
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)
{
size_t spawnedEntitiesCount = ticket.m_spawnedEntities.size();
const Spawnable::EntityList& entities = ticket.m_spawnable->GetEntities();
size_t entitiesSize = entities.size();
ticket.m_spawnedEntities.reserve(ticket.m_spawnedEntities.size() + entitiesSize);
ticket.m_spawnedEntityIndices.reserve(ticket.m_spawnedEntityIndices.size() + entitiesSize);
for(size_t i=0; i<entitiesSize; ++i)
{
ticket.m_spawnedEntities.push_back(SpawnSingleEntity(*entities[i], serializeContext));
ticket.m_spawnedEntityIndices.push_back(i);
}
if (request.m_completionCallback)
{
request.m_completionCallback(*request.m_ticket, SpawnableConstEntityContainerView(
ticket.m_spawnedEntities.begin() + spawnedEntitiesCount, ticket.m_spawnedEntities.end()));
}
ticket.m_currentTicketId++;
return true;
}
else
{
return false;
}
}
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)
{
size_t spawnedEntitiesCount = ticket.m_spawnedEntities.size();
const Spawnable::EntityList& entities = ticket.m_spawnable->GetEntities();
size_t entitiesSize = entities.size();
ticket.m_spawnedEntities.reserve(ticket.m_spawnedEntities.size() + entitiesSize);
ticket.m_spawnedEntityIndices.reserve(ticket.m_spawnedEntityIndices.size() + entitiesSize);
for (size_t index : request.m_entityIndices)
{
ticket.m_spawnedEntities.push_back(SpawnSingleEntity(*entities[index], serializeContext));
ticket.m_spawnedEntityIndices.push_back(index);
}
ticket.m_loadAll = false;
if (request.m_completionCallback)
{
request.m_completionCallback(*request.m_ticket, SpawnableConstEntityContainerView(
ticket.m_spawnedEntities.begin() + spawnedEntitiesCount, ticket.m_spawnedEntities.end()));
}
ticket.m_currentTicketId++;
return true;
}
else
{
return false;
}
}
bool SpawnableEntitiesManager::ProcessRequest(DespawnAllEntitiesCommand& request,
[[maybe_unused]] AZ::SerializeContext& serializeContext)
{
Ticket& ticket = GetTicketPayload<Ticket>(*request.m_ticket);
if (request.m_ticketId == ticket.m_currentTicketId)
{
for (AZ::Entity* entity : ticket.m_spawnedEntities)
{
if (entity != nullptr)
{
GameEntityContextRequestBus::Broadcast(
&GameEntityContextRequestBus::Events::DestroyGameEntityAndDescendants, entity->GetId());
}
}
ticket.m_spawnedEntities.clear();
ticket.m_spawnedEntityIndices.clear();
if (request.m_completionCallback)
{
request.m_completionCallback(*request.m_ticket);
}
ticket.m_currentTicketId++;
return true;
}
else
{
return false;
}
}
bool SpawnableEntitiesManager::ProcessRequest(ReloadSpawnableCommand& request, AZ::SerializeContext& serializeContext)
{
Ticket& ticket = GetTicketPayload<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)
{
// Delete the original entities.
for (AZ::Entity* entity : ticket.m_spawnedEntities)
{
if (entity != nullptr)
{
GameEntityContextRequestBus::Broadcast(
&GameEntityContextRequestBus::Events::DestroyGameEntityAndDescendants, entity->GetId());
}
}
// Rebuild the list of entities.
ticket.m_spawnedEntities.clear();
const Spawnable::EntityList& entities = request.m_spawnable->GetEntities();
if (ticket.m_loadAll)
{
// The new spawnable may have a different number of entities and since the intent of the user was
// to load every, simply start over.
ticket.m_spawnedEntityIndices.clear();
size_t entitiesSize = entities.size();
for (size_t i = 0; i < entitiesSize; ++i)
{
ticket.m_spawnedEntities.push_back(SpawnSingleEntity(*entities[i], serializeContext));
ticket.m_spawnedEntityIndices.push_back(i);
}
}
else
{
size_t entitiesSize = entities.size();
for (size_t index : ticket.m_spawnedEntityIndices)
{
ticket.m_spawnedEntities.push_back(
index < entitiesSize ? SpawnSingleEntity(*entities[index], serializeContext) : nullptr);
}
}
ticket.m_spawnable = AZStd::move(request.m_spawnable);
if (request.m_completionCallback)
{
request.m_completionCallback(*request.m_ticket, SpawnableConstEntityContainerView(
ticket.m_spawnedEntities.begin(), ticket.m_spawnedEntities.end()));
}
ticket.m_currentTicketId++;
return true;
}
else
{
return false;
}
}
bool SpawnableEntitiesManager::ProcessRequest(ListEntitiesCommand& request, [[maybe_unused]] AZ::SerializeContext& serializeContext)
{
Ticket& ticket = GetTicketPayload<Ticket>(*request.m_ticket);
if (request.m_ticketId == ticket.m_currentTicketId)
{
request.m_listCallback(*request.m_ticket, SpawnableConstEntityContainerView(
ticket.m_spawnedEntities.begin(), ticket.m_spawnedEntities.end()));
ticket.m_currentTicketId++;
return true;
}
else
{
return false;
}
}
bool SpawnableEntitiesManager::ProcessRequest(ClaimEntitiesCommand& request, [[maybe_unused]] AZ::SerializeContext& serializeContext)
{
Ticket& ticket = GetTicketPayload<Ticket>(*request.m_ticket);
if (request.m_ticketId == ticket.m_currentTicketId)
{
request.m_listCallback(*request.m_ticket, SpawnableEntityContainerView(
ticket.m_spawnedEntities.begin(), ticket.m_spawnedEntities.end()));
ticket.m_spawnedEntities.clear();
ticket.m_spawnedEntityIndices.clear();
ticket.m_currentTicketId++;
return true;
}
else
{
return false;
}
}
bool SpawnableEntitiesManager::ProcessRequest(BarrierCommand& request, [[maybe_unused]] AZ::SerializeContext& serializeContext)
{
Ticket& ticket = GetTicketPayload<Ticket>(*request.m_ticket);
if (request.m_ticketId == ticket.m_currentTicketId)
{
if (request.m_completionCallback)
{
request.m_completionCallback(*request.m_ticket);
}
ticket.m_currentTicketId++;
return true;
}
else
{
return false;
}
}
bool SpawnableEntitiesManager::ProcessRequest(DestroyTicketCommand& request, [[maybe_unused]] AZ::SerializeContext& serializeContext)
{
if (request.m_ticketId == request.m_ticket->m_currentTicketId)
{
for (AZ::Entity* entity : request.m_ticket->m_spawnedEntities)
{
if (entity != nullptr)
{
GameEntityContextRequestBus::Broadcast(
&GameEntityContextRequestBus::Events::DestroyGameEntityAndDescendants, entity->GetId());
}
}
delete request.m_ticket;
return true;
}
else
{
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
@@ -0,0 +1,160 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzCore/Memory/PoolAllocator.h>
#include <AzCore/std/limits.h>
#include <AzCore/std/containers/queue.h>
#include <AzCore/std/containers/deque.h>
#include <AzCore/std/containers/variant.h>
#include <AzCore/std/containers/vector.h>
#include <AzCore/std/parallel/mutex.h>
#include <AzFramework/Spawnable/SpawnableEntitiesInterface.h>
namespace AZ
{
class Entity;
class SerializeContext;
}
namespace AzFramework
{
class SpawnableEntitiesManager
: public SpawnableEntitiesInterface::Registrar
{
public:
AZ_RTTI(AzFramework::SpawnableEntitiesManager, "{6E14333F-128C-464C-94CA-A63B05A5E51C}");
enum class CommandQueueStatus : bool
{
HasCommandsLeft,
NoCommandLeft
};
~SpawnableEntitiesManager() override = default;
//
// The following functions are thread safe
//
void SpawnAllEntities(EntitySpawnTicket& ticket, EntitySpawnCallback completionCallback = {}) override;
void SpawnEntities(EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices,
EntitySpawnCallback completionCallback = {}) override;
void DespawnAllEntities(EntitySpawnTicket& ticket, EntityDespawnCallback completionCallback = {}) override;
void ReloadSpawnable(EntitySpawnTicket& ticket, AZ::Data::Asset<Spawnable> spawnable,
ReloadSpawnableCallback completionCallback = {}) override;
void ListEntities(EntitySpawnTicket& ticket, ListEntitiesCallback listCallback) override;
void ClaimEntities(EntitySpawnTicket& ticket, ClaimEntitiesCallback listCallback) override;
void Barrier(EntitySpawnTicket& spawnInfo, BarrierCallback completionCallback) override;
//
// The following function is thread safe but intended to be run from the main thread.
//
CommandQueueStatus ProcessQueue();
protected:
void* CreateTicket(AZ::Data::Asset<Spawnable>&& spawnable) override;
void DestroyTicket(void* ticket) override;
private:
struct Ticket
{
AZ_CLASS_ALLOCATOR(Ticket, AZ::ThreadPoolAllocator, 0);
static constexpr uint32_t Processing = AZStd::numeric_limits<uint32_t>::max();
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.
bool m_loadAll{ true };
};
struct SpawnAllEntitiesCommand
{
EntitySpawnCallback m_completionCallback;
EntitySpawnTicket* m_ticket;
uint32_t m_ticketId;
};
struct SpawnEntitiesCommand
{
AZStd::vector<size_t> m_entityIndices;
EntitySpawnCallback m_completionCallback;
EntitySpawnTicket* m_ticket;
uint32_t m_ticketId;
};
struct DespawnAllEntitiesCommand
{
EntityDespawnCallback m_completionCallback;
EntitySpawnTicket* m_ticket;
uint32_t m_ticketId;
};
struct ReloadSpawnableCommand
{
AZ::Data::Asset<Spawnable> m_spawnable;
ReloadSpawnableCallback m_completionCallback;
EntitySpawnTicket* m_ticket;
uint32_t m_ticketId;
};
struct ListEntitiesCommand
{
ListEntitiesCallback m_listCallback;
EntitySpawnTicket* m_ticket;
uint32_t m_ticketId;
};
struct ClaimEntitiesCommand
{
ClaimEntitiesCallback m_listCallback;
EntitySpawnTicket* m_ticket;
uint32_t m_ticketId;
};
struct BarrierCommand
{
BarrierCallback m_completionCallback;
EntitySpawnTicket* m_ticket;
uint32_t m_ticketId;
};
struct DestroyTicketCommand
{
Ticket* m_ticket;
uint32_t m_ticketId;
};
using Requests = AZStd::variant<SpawnAllEntitiesCommand, SpawnEntitiesCommand, DespawnAllEntitiesCommand, ReloadSpawnableCommand,
ListEntitiesCommand, ClaimEntitiesCommand, BarrierCommand, DestroyTicketCommand>;
AZ::Entity* SpawnSingleEntity(const AZ::Entity& entityTemplate, AZ::SerializeContext& serializeContext);
bool ProcessRequest(SpawnAllEntitiesCommand& request, AZ::SerializeContext& serializeContext);
bool ProcessRequest(SpawnEntitiesCommand& request, AZ::SerializeContext& serializeContext);
bool ProcessRequest(DespawnAllEntitiesCommand& request, AZ::SerializeContext& serializeContext);
bool ProcessRequest(ReloadSpawnableCommand& request, AZ::SerializeContext& serializeContext);
bool ProcessRequest(ListEntitiesCommand& request, AZ::SerializeContext& serializeContext);
bool ProcessRequest(ClaimEntitiesCommand& request, AZ::SerializeContext& serializeContext);
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;
};
} // namespace AzFramework
@@ -0,0 +1,158 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzFramework/Spawnable/SpawnableMonitor.h>
namespace AzFramework
{
SpawnableMonitor::SpawnableMonitor(AZ::Data::AssetId spawnableAssetId)
{
AZ::Data::AssetBus::MultiHandler::BusConnect(spawnableAssetId);
m_isConnected = true;
}
SpawnableMonitor::~SpawnableMonitor()
{
Disconnect();
}
bool SpawnableMonitor::Connect(AZ::Data::AssetId spawnableAssetId)
{
if (!m_isConnected)
{
AZ::Data::AssetBus::MultiHandler::BusConnect(spawnableAssetId);
m_isConnected = true;
return true;
}
else
{
return false;
}
}
bool SpawnableMonitor::Disconnect()
{
if (m_isConnected)
{
AZ::Data::AssetBus::MultiHandler::BusDisconnect();
m_isLoaded = false;
m_isConnected = false;
return true;
}
else
{
return false;
}
}
bool SpawnableMonitor::IsConnected() const
{
return m_isConnected;
}
bool SpawnableMonitor::IsLoaded() const
{
return m_isLoaded;
}
void SpawnableMonitor::OnSpawnableLoaded()
{
}
void SpawnableMonitor::OnSpawnableUnloaded()
{
}
void SpawnableMonitor::OnSpawnableReloaded([[maybe_unused]] AZ::Data::Asset<Spawnable>&& replacementAsset)
{
OnSpawnableUnloaded();
OnSpawnableLoaded();
}
void SpawnableMonitor::OnSpawnableIssue([[maybe_unused]] IssueType issueType, [[maybe_unused]] AZStd::string_view message)
{
switch (issueType)
{
case IssueType::Error:
AZ_Error("Spawnables", false, "%.*s", AZ_STRING_ARG(message));
break;
case IssueType::Cancel:
AZ_TracePrintf("Spawnables", "%.*s", AZ_STRING_ARG(message));
break;
case IssueType::ReloadError:
AZ_TracePrintf("Spawnables", "%.*s", AZ_STRING_ARG(message));
break;
}
}
void SpawnableMonitor::OnAssetReady(AZ::Data::Asset<AZ::Data::AssetData> asset)
{
AZ_Assert(!m_isLoaded, "Trying to load spawnable %s (%s) for a second time.",
asset.GetHint().c_str(), asset.GetId().ToString<AZStd::string>().c_str());
m_isLoaded = true;
OnSpawnableLoaded();
}
void SpawnableMonitor::OnAssetReloaded(AZ::Data::Asset<AZ::Data::AssetData> asset)
{
AZ_Assert(m_isLoaded, "Trying to reload a spawnable %s (%s) that wasn't loaded.",
asset.GetHint().c_str(), asset.GetId().ToString<AZStd::string>().c_str());
OnSpawnableReloaded(AZStd::move(asset));
}
void SpawnableMonitor::OnAssetUnloaded([[maybe_unused]] const AZ::Data::AssetId assetId,
[[maybe_unused]] const AZ::Data::AssetType assetType)
{
AZ_Assert(m_isLoaded, "Trying to unload a spawnable %s that was never loaded.", assetId.ToString<AZStd::string>().c_str());
m_isLoaded = false;
OnSpawnableUnloaded();
}
void SpawnableMonitor::OnAssetError(AZ::Data::Asset<AZ::Data::AssetData> asset)
{
if (m_isLoaded)
{
m_isLoaded = false;
OnSpawnableUnloaded();
}
AZStd::string message = AZStd::string::format("An error occurred during the loading of spawnable '%s' (%s).",
asset.GetHint().c_str(), asset.GetId().ToString<AZStd::string>().c_str());
OnSpawnableIssue(IssueType::Error, message);
}
void SpawnableMonitor::OnAssetCanceled(AZ::Data::AssetId assetId)
{
if (m_isLoaded)
{
m_isLoaded = false;
OnSpawnableUnloaded();
}
AZStd::string message = AZStd::string::format("The loading of spawnable '%s' was canceled.",
assetId.ToString<AZStd::string>().c_str());
OnSpawnableIssue(IssueType::Cancel, message);
}
void SpawnableMonitor::OnAssetReloadError(AZ::Data::Asset<AZ::Data::AssetData> asset)
{
if (m_isLoaded)
{
m_isLoaded = false;
OnSpawnableUnloaded();
}
AZStd::string message = AZStd::string::format("An error occurred while trying to reload spawnable '%s' (%s).",
asset.GetHint().c_str(), asset.GetId().ToString<AZStd::string>().c_str());
OnSpawnableIssue(IssueType::ReloadError, message);
}
} // namespace AzFramework
@@ -0,0 +1,78 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzCore/Asset/AssetCommon.h>
#include <AzCore/std/functional.h>
#include <AzCore/std/string/string_view.h>
#include <AzFramework/Spawnable/Spawnable.h>
namespace AzFramework
{
//! A utility class to make it easier to work with individual spawnables.
//! This class will monitor a spawnable for changes and provides a simplified interface to
//! respond to.
class SpawnableMonitor
: public AZ::Data::AssetBus::MultiHandler
{
public:
enum IssueType
{
Error,
Cancel,
ReloadError
};
SpawnableMonitor() = default;
//! Constructs the monitor to immediately start monitoring a specific spawnable. Depending on the state of the
//! spawnable this can result in immediate calls to OnSpawnable* functions.
explicit SpawnableMonitor(AZ::Data::AssetId spawnableAssetId);
//! Destroys the monitor, including disconnecting from the spawnable if connected.
virtual ~SpawnableMonitor();
//! Connects the monitor to start monitoring a specific spawnable. Depending on the state of the spawnable this
//! can result in immediate calls to OnSpawnable* functions. If the monitor is already connected this will do
//! nothing and return false.
virtual bool Connect(AZ::Data::AssetId spawnableAssetId);
//! Disconnects from the monitor if connected and otherwise does nothing and returns false.
virtual bool Disconnect();
//! Returns true if the monitor is connected and otherwise false.
[[nodiscard]] virtual bool IsConnected() const;
//! Returns true if the monitor tracked the spawnable able being ready for use, otherwise false.
[[nodiscard]] virtual bool IsLoaded() const;
protected:
//! Called when a spawnable has been loaded and is ready for used.
virtual void OnSpawnableLoaded();
//! Called when an spawnable has been unloaded and should no longer be used.
virtual void OnSpawnableUnloaded();
//! Called when a spawnable has been reloaded. Note that if the replacement isn't used to replace
//! the original spawnable then the replacement asset will eventually run out of references and send
//! an OnSpawnableUnloaded event.
virtual void OnSpawnableReloaded(AZ::Data::Asset<Spawnable>&& replacementAsset);
//! Called when an error occurred while the Asset Manager did an operation on the spawnable.
virtual void OnSpawnableIssue(IssueType issueType, AZStd::string_view message);
private:
void OnAssetReady(AZ::Data::Asset<AZ::Data::AssetData> asset) override;
void OnAssetReloaded(AZ::Data::Asset<AZ::Data::AssetData> asset) override;
void OnAssetUnloaded(const AZ::Data::AssetId assetId, const AZ::Data::AssetType assetType) override;
void OnAssetError(AZ::Data::Asset<AZ::Data::AssetData> asset) override;
void OnAssetCanceled(AZ::Data::AssetId assetId) override;
void OnAssetReloadError(AZ::Data::Asset<AZ::Data::AssetData> asset) override;
bool m_isConnected{ false };
bool m_isLoaded{ false };
};
} // namespace AzFramework
@@ -46,30 +46,82 @@ namespace AzFramework
services.push_back(AZ_CRC_CE("AssetCatalogService"));
}
void SpawnableSystemComponent::OnTick(float /*deltaTime*/, AZ::ScriptTimePoint /*time*/)
{
m_entitiesManager.ProcessQueue();
RootSpawnableNotificationBus::ExecuteQueuedEvents();
}
void SpawnableSystemComponent::OnCatalogLoaded([[maybe_unused]] const char* catalogFile)
{
if (!m_rootSpawnableInitialized)
if (!m_catalogAvailable)
{
auto registry = AZ::SettingsRegistry::Get();
AZ_Assert(registry, "Unable to check for root spawnable because the Settings Registry is not available.");
if (registry->GetObject(m_rootSpawnable, RootSpawnableRegistryKey) && m_rootSpawnable.GetId().IsValid())
{
AZ_TracePrintf("Spawnables", "Root spawnable '%s' used.\n", m_rootSpawnable.GetHint().c_str());
if (!m_rootSpawnable.QueueLoad())
{
AZ_Error("Spawnables", false, "Unable to queue root spawnable for loading.\n");
}
}
else
{
AZ_Warning("Spawnables", false, "No root spawnable assigned or root spawanble couldnt' be loaded.\n"
"The root spawnable can be assigned in the Settings Registry under the key '%s'.\n", RootSpawnableRegistryKey);
}
m_rootSpawnableInitialized = true;
m_catalogAvailable = true;
LoadRootSpawnableFromSettingsRegistry();
}
}
uint64_t SpawnableSystemComponent::AssignRootSpawnable(AZ::Data::Asset<Spawnable> rootSpawnable)
{
uint64_t generation = 0;
if (m_rootSpawnableId == rootSpawnable.GetId())
{
AZ_TracePrintf("Spawnables", "Root spawnable wasn't updated because it's already assigned to the requested asset.");
return m_rootSpawnableContainer.GetCurrentGeneration();
}
if (rootSpawnable.QueueLoad())
{
m_rootSpawnableId = rootSpawnable.GetId();
// Suspend and resume processing in the container that completion calls aren't received until
// everything has been setup to accept callbacks from the call.
m_rootSpawnableContainer.Reset(rootSpawnable);
m_rootSpawnableContainer.SpawnAllEntities();
generation = m_rootSpawnableContainer.GetCurrentGeneration();
AZ_TracePrintf("Spawnables", "Root spawnable set to '%s' at generation %zu.\n", rootSpawnable.GetHint().c_str(),
generation);
m_rootSpawnableContainer.Alert(
[newSpawnable = AZStd::move(rootSpawnable)](uint32_t generation)
{
RootSpawnableNotificationBus::QueueBroadcast(
&RootSpawnableNotificationBus::Events::OnRootSpawnableAssigned, newSpawnable, generation);
});
}
else
{
AZ_Error("Spawnables", false, "Unable to queue root spawnable '%s' for loading.", rootSpawnable.GetHint().c_str());
}
return generation;
}
void SpawnableSystemComponent::ReleaseRootSpawnable()
{
if (m_rootSpawnableContainer.IsSet())
{
m_rootSpawnableContainer.Alert(
[](uint32_t generation)
{
RootSpawnableNotificationBus::QueueBroadcast(&RootSpawnableNotificationBus::Events::OnRootSpawnableReleased, generation);
});
m_rootSpawnableContainer.Clear();
}
m_rootSpawnableId = AZ::Data::AssetId();
}
void SpawnableSystemComponent::OnRootSpawnableAssigned([[maybe_unused]] AZ::Data::Asset<Spawnable> rootSpawnable,
[[maybe_unused]] uint32_t generation)
{
AZ_TracePrintf("Spawnables", "New root spawnable '%s' assigned (generation: %i).\n", rootSpawnable.GetHint().c_str(), generation);
}
void SpawnableSystemComponent::OnRootSpawnableReleased([[maybe_unused]] uint32_t generation)
{
AZ_TracePrintf("Spawnables", "Generation %i of the root spawnable has been released.\n", generation);
}
void SpawnableSystemComponent::Activate()
{
// Register with AssetDatabase
@@ -83,14 +135,105 @@ namespace AzFramework
&AZ::Data::AssetCatalogRequestBus::Events::AddExtension, Spawnable::FileExtension);
AssetCatalogEventBus::Handler::BusConnect();
RootSpawnableNotificationBus::Handler::BusConnect();
AZ::TickBus::Handler::BusConnect();
auto registry = AZ::SettingsRegistry::Get();
AZ_Assert(registry, "Unable to change root spawnable callback because Settings Registry is not available.");
m_registryChangeHandler = registry->RegisterNotifier([this](AZStd::string_view path, AZ::SettingsRegistryInterface::Type /*type*/)
{
if (path.starts_with(RootSpawnableRegistryKey))
{
LoadRootSpawnableFromSettingsRegistry();
}
});
}
void SpawnableSystemComponent::Deactivate()
{
m_registryChangeHandler.Disconnect();
AZ::TickBus::Handler::BusDisconnect();
RootSpawnableNotificationBus::Handler::BusDisconnect();
AssetCatalogEventBus::Handler::BusDisconnect();
AZ_Assert(AZ::Data::AssetManager::IsReady(),
"Spawnables can't be unregistered because the Asset Manager has been destroyed already or never started.");
if (m_catalogAvailable)
{
ReleaseRootSpawnable();
// The SpawnalbleSystemComponent needs to guarantee there's no more processing left to do by the
// entity manager before it can safely destroy it on shutdown, but also to make sure that are no
// more calls to the callback registered to the root spawnable as that accesses this component.
m_rootSpawnableContainer.Clear();
SpawnableEntitiesManager::CommandQueueStatus queueStatus;
do
{
queueStatus = m_entitiesManager.ProcessQueue();
} while (queueStatus == SpawnableEntitiesManager::CommandQueueStatus::HasCommandsLeft);
}
AZ::Data::AssetManager::Instance().UnregisterHandler(&m_assetHandler);
}
void SpawnableSystemComponent::LoadRootSpawnableFromSettingsRegistry()
{
AZ_Assert(m_catalogAvailable, "Attempting to load root spawnable while the catalog is not available yet.");
auto registry = AZ::SettingsRegistry::Get();
AZ_Assert(registry, "Unable to check for root spawnable because the Settings Registry is not available.");
AZ::SettingsRegistryInterface::Type rootSpawnableKeyType = registry->GetType(RootSpawnableRegistryKey);
if (rootSpawnableKeyType == AZ::SettingsRegistryInterface::Type::Object)
{
AZ::Data::Asset<Spawnable> rootSpawnable;
if (registry->GetObject(rootSpawnable, RootSpawnableRegistryKey) && rootSpawnable.GetId().IsValid())
{
AssignRootSpawnable(AZStd::move(rootSpawnable));
}
else
{
AZ_Warning("Spawnables", false, "Root spawnable couldn't be queued for loading");
ReleaseRootSpawnable();
}
}
else if (rootSpawnableKeyType == AZ::SettingsRegistryInterface::Type::String)
{
AZStd::string rootSpawnableName;
if (registry->Get(rootSpawnableName, RootSpawnableRegistryKey))
{
AZ::Data::AssetId rootSpawnableId;
AZ::Data::AssetCatalogRequestBus::BroadcastResult(
rootSpawnableId, &AZ::Data::AssetCatalogRequestBus::Events::GetAssetIdByPath, rootSpawnableName.c_str(),
azrtti_typeid<Spawnable>(), false);
if (rootSpawnableId.IsValid())
{
AZ::Data::Asset<Spawnable> rootSpawnable = AZ::Data::Asset<Spawnable>(rootSpawnableId, azrtti_typeid<Spawnable>());
if (rootSpawnable.GetId().IsValid())
{
AssignRootSpawnable(AZStd::move(rootSpawnable));
}
else
{
AZ_Warning(
"Spawnables", false, "Root spawnable at '%s' couldn't be queued for loading.", rootSpawnableName.c_str());
ReleaseRootSpawnable();
}
}
else
{
AZ_Warning(
"Spawnables", false, "Root spawnable with name '%s' wasn't found in the asset catalog.", rootSpawnableName.c_str());
ReleaseRootSpawnable();
}
}
}
else if (rootSpawnableKeyType == AZ::SettingsRegistryInterface::Type::NoType)
{
AZ_Warning(
"Spawnables", false,
"No root spawnable assigned. The root spawnable can be assigned in the Settings Registry under the key '%s'.\n",
RootSpawnableRegistryKey);
ReleaseRootSpawnable();
}
}
} // namespace AzFramework
@@ -13,15 +13,24 @@
#pragma once
#include <AzCore/Component/Component.h>
#include <AzCore/Component/TickBus.h>
#include <AzCore/Settings/SettingsRegistry.h>
#include <AzCore/std/parallel/atomic.h>
#include <AzFramework/Asset/AssetCatalogBus.h>
#include <AzFramework/Spawnable/RootSpawnableInterface.h>
#include <AzFramework/Spawnable/Spawnable.h>
#include <AzFramework/Spawnable/SpawnableAssetHandler.h>
#include <AzFramework/Spawnable/SpawnableEntitiesContainer.h>
#include <AzFramework/Spawnable/SpawnableEntitiesManager.h>
namespace AzFramework
{
class SpawnableSystemComponent
: public AZ::Component
, public AZ::TickBus::Handler
, public AssetCatalogEventBus::Handler
, public RootSpawnableInterface::Registrar
, public RootSpawnableNotificationBus::Handler
{
public:
AZ_COMPONENT(SpawnableSystemComponent, "{12D0DA52-BB86-4AC3-8862-9493E0D0E207}");
@@ -35,23 +44,53 @@ namespace AzFramework
SpawnableSystemComponent& operator=(const SpawnableSystemComponent&) = delete;
SpawnableSystemComponent& operator=(SpawnableSystemComponent&&) = delete;
//
// Component
//
static void Reflect(AZ::ReflectContext* context);
static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services);
static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& services);
static void GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& services);
//
// TickBus
//
void OnTick(float deltaTime, AZ::ScriptTimePoint time) override;
//
// AssetCatalogEventBus
//
void OnCatalogLoaded(const char* catalogFile) override;
//
// RootSpawnableInterface
//
uint64_t AssignRootSpawnable(AZ::Data::Asset<Spawnable> rootSpawnable) override;
void ReleaseRootSpawnable() override;
//
// RootSpawnbleNotificationBus
//
void OnRootSpawnableAssigned(AZ::Data::Asset<Spawnable> rootSpawnable, uint32_t generation) override;
void OnRootSpawnableReleased(uint32_t generation) override;
protected:
void Activate() override;
void Deactivate() override;
void LoadRootSpawnableFromSettingsRegistry();
SpawnableAssetHandler m_assetHandler;
AZ::Data::Asset<Spawnable> m_rootSpawnable;
bool m_rootSpawnableInitialized{ false };
SpawnableEntitiesManager m_entitiesManager;
SpawnableEntitiesContainer m_rootSpawnableContainer;
AZ::SettingsRegistryInterface::NotifyEventHandler m_registryChangeHandler;
AZ::Data::AssetId m_rootSpawnableId;
bool m_catalogAvailable{ false };
};
} // namespace AzFramework