Integrating up through commit 90f050496
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user