diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.cpp index a528797f63..97169ebeb3 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.cpp @@ -14,6 +14,10 @@ namespace AzFramework { + // + // SpawnableEntityContainerView + // + SpawnableEntityContainerView::SpawnableEntityContainerView(AZ::Entity** begin, size_t length) : m_begin(begin) , m_end(begin + length) @@ -52,6 +56,9 @@ namespace AzFramework } + // + // SpawnableConstEntityContainerView + // SpawnableConstEntityContainerView::SpawnableConstEntityContainerView(AZ::Entity** begin, size_t length) : m_begin(begin) @@ -91,6 +98,136 @@ namespace AzFramework } + // + // SpawnableIndexEntityPair + // + + SpawnableIndexEntityPair::SpawnableIndexEntityPair(AZ::Entity** entityIterator, size_t* indexIterator) + : m_entity(entityIterator) + , m_index(indexIterator) + { + } + + AZ::Entity* SpawnableIndexEntityPair::GetEntity() + { + return *m_entity; + } + + const AZ::Entity* SpawnableIndexEntityPair::GetEntity() const + { + return *m_entity; + } + + size_t SpawnableIndexEntityPair::GetIndex() const + { + return *m_index; + } + + // + // SpawnableIndexEntityIterator + // + + SpawnableIndexEntityIterator::SpawnableIndexEntityIterator(AZ::Entity** entityIterator, size_t* indexIterator) + : m_value(entityIterator, indexIterator) + { + } + + SpawnableIndexEntityIterator& SpawnableIndexEntityIterator::operator++() + { + ++m_value.m_entity; + ++m_value.m_index; + return *this; + } + + SpawnableIndexEntityIterator SpawnableIndexEntityIterator::operator++(int) + { + SpawnableIndexEntityIterator result = *this; + ++m_value.m_entity; + ++m_value.m_index; + return result; + } + + SpawnableIndexEntityIterator& SpawnableIndexEntityIterator::operator--() + { + --m_value.m_entity; + --m_value.m_index; + return *this; + } + + SpawnableIndexEntityIterator SpawnableIndexEntityIterator::operator--(int) + { + SpawnableIndexEntityIterator result = *this; + --m_value.m_entity; + --m_value.m_index; + return result; + } + + bool SpawnableIndexEntityIterator::operator==(const SpawnableIndexEntityIterator& rhs) + { + return m_value.m_entity == rhs.m_value.m_entity && m_value.m_index == rhs.m_value.m_index; + } + + bool SpawnableIndexEntityIterator::operator!=(const SpawnableIndexEntityIterator& rhs) + { + return m_value.m_entity != rhs.m_value.m_entity || m_value.m_index != rhs.m_value.m_index; + } + + SpawnableIndexEntityPair& SpawnableIndexEntityIterator::operator*() + { + return m_value; + } + + const SpawnableIndexEntityPair& SpawnableIndexEntityIterator::operator*() const + { + return m_value; + } + + SpawnableIndexEntityPair* SpawnableIndexEntityIterator::operator->() + { + return &m_value; + } + + const SpawnableIndexEntityPair* SpawnableIndexEntityIterator::operator->() const + { + return &m_value; + } + + + // + // SpawnableConstIndexEntityContainerView + // + + SpawnableConstIndexEntityContainerView::SpawnableConstIndexEntityContainerView( + AZ::Entity** beginEntity, size_t* beginIndices, size_t length) + : m_begin(beginEntity, beginIndices) + , m_end(beginEntity + length, beginIndices + length) + { + } + + const SpawnableIndexEntityIterator& SpawnableConstIndexEntityContainerView::begin() + { + return m_begin; + } + + const SpawnableIndexEntityIterator& SpawnableConstIndexEntityContainerView::end() + { + return m_end; + } + + const SpawnableIndexEntityIterator& SpawnableConstIndexEntityContainerView::cbegin() + { + return m_begin; + } + + const SpawnableIndexEntityIterator& SpawnableConstIndexEntityContainerView::cend() + { + return m_end; + } + + + // + // EntitySpawnTicket + // EntitySpawnTicket::EntitySpawnTicket(EntitySpawnTicket&& rhs) : m_payload(rhs.m_payload) diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h index ac66288ff2..93ca2f0bd9 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h @@ -58,6 +58,72 @@ namespace AzFramework AZ::Entity** m_end; }; + class SpawnableIndexEntityPair + { + public: + friend class SpawnableIndexEntityIterator; + + AZ::Entity* GetEntity(); + const AZ::Entity* GetEntity() const; + size_t GetIndex() const; + + private: + SpawnableIndexEntityPair() = default; + SpawnableIndexEntityPair(const SpawnableIndexEntityPair&) = default; + SpawnableIndexEntityPair(SpawnableIndexEntityPair&&) = default; + SpawnableIndexEntityPair(AZ::Entity** entityIterator, size_t* indexIterator); + + SpawnableIndexEntityPair& operator=(const SpawnableIndexEntityPair&) = default; + SpawnableIndexEntityPair& operator=(SpawnableIndexEntityPair&&) = default; + + AZ::Entity** m_entity { nullptr }; + size_t* m_index { nullptr }; + }; + + class SpawnableIndexEntityIterator + { + public: + // Limited to bidirectional iterator as there's no use case for extending it further, but can be extended if a use case is found. + using iterator_category = AZStd::bidirectional_iterator_tag; + using value_type = SpawnableIndexEntityPair; + using difference_type = size_t; + using pointer = SpawnableIndexEntityPair*; + using reference = SpawnableIndexEntityPair&; + + SpawnableIndexEntityIterator(AZ::Entity** entityIterator, size_t* indexIterator); + + SpawnableIndexEntityIterator& operator++(); + SpawnableIndexEntityIterator operator++(int); + SpawnableIndexEntityIterator& operator--(); + SpawnableIndexEntityIterator operator--(int); + + bool operator==(const SpawnableIndexEntityIterator& rhs); + bool operator!=(const SpawnableIndexEntityIterator& rhs); + + SpawnableIndexEntityPair& operator*(); + const SpawnableIndexEntityPair& operator*() const; + SpawnableIndexEntityPair* operator->(); + const SpawnableIndexEntityPair* operator->() const; + + private: + SpawnableIndexEntityPair m_value; + }; + + class SpawnableConstIndexEntityContainerView + { + public: + SpawnableConstIndexEntityContainerView(AZ::Entity** beginEntity, size_t* beginIndices, size_t length); + + const SpawnableIndexEntityIterator& begin(); + const SpawnableIndexEntityIterator& end(); + const SpawnableIndexEntityIterator& cbegin(); + const SpawnableIndexEntityIterator& cend(); + + private: + SpawnableIndexEntityIterator m_begin; + SpawnableIndexEntityIterator m_end; + }; + //! Requests to the SpawnableEntitiesInterface require a ticket with a valid spawnable that be used as a template. A ticket can //! be reused for multiple calls on the same spawnable and is safe to use by multiple threads at the same time. Entities created //! from the spawnable may be tracked by the ticket and so using the same ticket is needed to despawn the exact entities created @@ -88,6 +154,7 @@ namespace AzFramework using EntityDespawnCallback = AZStd::function; using ReloadSpawnableCallback = AZStd::function; using ListEntitiesCallback = AZStd::function; + using ListIndicesEntitiesCallback = AZStd::function; using ClaimEntitiesCallback = AZStd::function; using BarrierCallback = AZStd::function; @@ -140,6 +207,12 @@ namespace AzFramework //! @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; + //! List all entities that are spawned using this ticket with their spawnable index. + //! The index will be of the template in the spawnable used to create the entity instance from. The same template can be used + //! for multiple entities so the same index may appear multiple times. + //! @param ticket Only the entities associated with this ticket will be listed. + //! @param listCallback Required callback that will be called to list the entities and indices on. + virtual void ListIndicesAndEntities(EntitySpawnTicket& ticket, ListIndicesEntitiesCallback listCallback) = 0; //! Claim all entities that are spawned using this ticket. Ownership of the entities is transferred from the ticket to the //! caller through the callback. After this call the ticket will have no entities associated with it. The caller of //! this function will need to manage the entities after this call. diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp index 8045766686..7e20f7b265 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp @@ -25,6 +25,8 @@ namespace AzFramework void SpawnableEntitiesManager::SpawnAllEntities(EntitySpawnTicket& ticket, EntityPreInsertionCallback preInsertionCallback, EntitySpawnCallback completionCallback) { + AZ_Assert(ticket.IsValid(), "Ticket provided to SpawnAllEntities hasn't been initialized."); + SpawnAllEntitiesCommand queueEntry; queueEntry.m_ticket = &ticket; queueEntry.m_completionCallback = AZStd::move(completionCallback); @@ -40,6 +42,8 @@ namespace AzFramework EntitySpawnTicket& ticket, AZStd::vector entityIndices, EntityPreInsertionCallback preInsertionCallback, EntitySpawnCallback completionCallback) { + AZ_Assert(ticket.IsValid(), "Ticket provided to SpawnEntities hasn't been initialized."); + SpawnEntitiesCommand queueEntry; queueEntry.m_ticket = &ticket; queueEntry.m_entityIndices = AZStd::move(entityIndices); @@ -54,6 +58,8 @@ namespace AzFramework void SpawnableEntitiesManager::DespawnAllEntities(EntitySpawnTicket& ticket, EntityDespawnCallback completionCallback) { + AZ_Assert(ticket.IsValid(), "Ticket provided to DespawnAllEntities hasn't been initialized."); + DespawnAllEntitiesCommand queueEntry; queueEntry.m_ticket = &ticket; queueEntry.m_completionCallback = AZStd::move(completionCallback); @@ -67,6 +73,8 @@ namespace AzFramework void SpawnableEntitiesManager::ReloadSpawnable(EntitySpawnTicket& ticket, AZ::Data::Asset spawnable, ReloadSpawnableCallback completionCallback) { + AZ_Assert(ticket.IsValid(), "Ticket provided to ReloadSpawnable hasn't been initialized."); + ReloadSpawnableCommand queueEntry; queueEntry.m_ticket = &ticket; queueEntry.m_spawnable = AZStd::move(spawnable); @@ -81,6 +89,7 @@ namespace AzFramework void SpawnableEntitiesManager::ListEntities(EntitySpawnTicket& ticket, ListEntitiesCallback listCallback) { AZ_Assert(listCallback, "ListEntities called on spawnable entities without a valid callback to use."); + AZ_Assert(ticket.IsValid(), "Ticket provided to ListEntities hasn't been initialized."); ListEntitiesCommand queueEntry; queueEntry.m_ticket = &ticket; @@ -92,9 +101,25 @@ namespace AzFramework } } + void SpawnableEntitiesManager::ListIndicesAndEntities(EntitySpawnTicket& ticket, ListIndicesEntitiesCallback listCallback) + { + AZ_Assert(listCallback, "ListEntities called on spawnable entities without a valid callback to use."); + AZ_Assert(ticket.IsValid(), "Ticket provided to ListEntities hasn't been initialized."); + + ListIndicesEntitiesCommand queueEntry; + queueEntry.m_ticket = &ticket; + queueEntry.m_listCallback = AZStd::move(listCallback); + { + AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex); + queueEntry.m_ticketId = GetTicketPayload(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."); + AZ_Assert(ticket.IsValid(), "Ticket provided to ClaimEntities hasn't been initialized."); ClaimEntitiesCommand queueEntry; queueEntry.m_ticket = &ticket; @@ -109,6 +134,7 @@ namespace AzFramework void SpawnableEntitiesManager::Barrier(EntitySpawnTicket& ticket, BarrierCallback completionCallback) { AZ_Assert(completionCallback, "Barrier on spawnable entities called without a valid callback to use."); + AZ_Assert(ticket.IsValid(), "Ticket provided to Barrier hasn't been initialized."); BarrierCommand queueEntry; queueEntry.m_ticket = &ticket; @@ -499,6 +525,27 @@ namespace AzFramework } } + bool SpawnableEntitiesManager::ProcessRequest(ListIndicesEntitiesCommand& request, [[maybe_unused]] AZ::SerializeContext& serializeContext) + { + Ticket& ticket = GetTicketPayload(*request.m_ticket); + if (request.m_ticketId == ticket.m_currentTicketId) + { + AZ_Assert( + ticket.m_spawnedEntities.size() == ticket.m_spawnedEntityIndices.size(), + "Entities and indices on spawnable ticket have gone out of sync."); + request.m_listCallback( + *request.m_ticket, + SpawnableConstIndexEntityContainerView( + ticket.m_spawnedEntities.begin(), ticket.m_spawnedEntityIndices.begin(), ticket.m_spawnedEntities.size())); + ticket.m_currentTicketId++; + return true; + } + else + { + return false; + } + } + bool SpawnableEntitiesManager::ProcessRequest(ClaimEntitiesCommand& request, [[maybe_unused]] AZ::SerializeContext& serializeContext) { Ticket& ticket = GetTicketPayload(*request.m_ticket); diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h index e20f58ac76..3481ab180a 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h @@ -36,6 +36,7 @@ namespace AzFramework { public: AZ_RTTI(AzFramework::SpawnableEntitiesManager, "{6E14333F-128C-464C-94CA-A63B05A5E51C}"); + AZ_CLASS_ALLOCATOR(SpawnableEntitiesManager, AZ::SystemAllocator, 0); enum class CommandQueueStatus : bool { @@ -58,6 +59,7 @@ namespace AzFramework ReloadSpawnableCallback completionCallback = {}) override; void ListEntities(EntitySpawnTicket& ticket, ListEntitiesCallback listCallback) override; + void ListIndicesAndEntities(EntitySpawnTicket& ticket, ListIndicesEntitiesCallback listCallback) override; void ClaimEntities(EntitySpawnTicket& ticket, ClaimEntitiesCallback listCallback) override; void Barrier(EntitySpawnTicket& spawnInfo, BarrierCallback completionCallback) override; @@ -123,6 +125,12 @@ namespace AzFramework EntitySpawnTicket* m_ticket; uint32_t m_ticketId; }; + struct ListIndicesEntitiesCommand + { + ListIndicesEntitiesCallback m_listCallback; + EntitySpawnTicket* m_ticket; + uint32_t m_ticketId; + }; struct ClaimEntitiesCommand { ClaimEntitiesCallback m_listCallback; @@ -141,8 +149,9 @@ namespace AzFramework uint32_t m_ticketId; }; - using Requests = AZStd::variant; + using Requests = AZStd::variant< + SpawnAllEntitiesCommand, SpawnEntitiesCommand, DespawnAllEntitiesCommand, ReloadSpawnableCommand, ListEntitiesCommand, + ListIndicesEntitiesCommand, ClaimEntitiesCommand, BarrierCommand, DestroyTicketCommand>; AZ::Entity* SpawnSingleEntity(const AZ::Entity& entityTemplate, AZ::SerializeContext& serializeContext); @@ -155,6 +164,7 @@ namespace AzFramework bool ProcessRequest(DespawnAllEntitiesCommand& request, AZ::SerializeContext& serializeContext); bool ProcessRequest(ReloadSpawnableCommand& request, AZ::SerializeContext& serializeContext); bool ProcessRequest(ListEntitiesCommand& request, AZ::SerializeContext& serializeContext); + bool ProcessRequest(ListIndicesEntitiesCommand& 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); diff --git a/Code/Framework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp b/Code/Framework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp new file mode 100644 index 0000000000..1fa50fff52 --- /dev/null +++ b/Code/Framework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp @@ -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 +#include +#include +#include +#include + +namespace UnitTest +{ + class TestApplication : public AzFramework::Application + { + public: + // ComponentApplication + void SetSettingsRegistrySpecializations(AZ::SettingsRegistryInterface::Specializations& specializations) override + { + Application::SetSettingsRegistrySpecializations(specializations); + specializations.Append("test"); + specializations.Append("spawnable"); + } + }; + + class SpawnableEntitiesManagerTest : public AllocatorsFixture + { + public: + void SetUp() override + { + AllocatorsFixture::SetUp(); + + m_application = new TestApplication(); + AZ::ComponentApplication::Descriptor descriptor; + m_application->Start(descriptor); + + m_spawnable = aznew AzFramework::Spawnable( + AZ::Data::AssetId::CreateString("{EB2E8A2B-F253-4A90-BBF4-55F2EED786B8}:0"), AZ::Data::AssetData::AssetStatus::Ready); + m_spawnableAsset = new AZ::Data::Asset(m_spawnable, AZ::Data::AssetLoadBehavior::Default); + m_ticket = new AzFramework::EntitySpawnTicket(*m_spawnableAsset); + + auto managerInterface = AzFramework::SpawnableEntitiesInterface::Get(); + m_manager = azrtti_cast(managerInterface); + } + + void TearDown() override + { + delete m_ticket; + m_ticket = nullptr; + // One more tick on the spawnable entities manager in order to delete the ticket fully. + m_manager->ProcessQueue(); + + delete m_spawnableAsset; + m_spawnableAsset = nullptr; + // This will also delete m_spawnable. + + delete m_application; + m_application = nullptr; + + AllocatorsFixture::TearDown(); + } + + void FillSpawnable(size_t numElements) + { + AzFramework::Spawnable::EntityList& entities = m_spawnable->GetEntities(); + entities.reserve(numElements); + for (size_t i=0; i()); + } + } + + protected: + AZ::Data::Asset* m_spawnableAsset { nullptr }; + AzFramework::SpawnableEntitiesManager* m_manager { nullptr }; + AzFramework::EntitySpawnTicket* m_ticket { nullptr }; + AzFramework::Spawnable* m_spawnable { nullptr }; + TestApplication* m_application { nullptr }; + }; + + TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_Call_AllEntitiesSpawned) + { + static constexpr size_t NumEntities = 4; + FillSpawnable(NumEntities); + + size_t spawnedEntitiesCount = 0; + auto callback = + [&spawnedEntitiesCount](AzFramework::EntitySpawnTicket&, AzFramework::SpawnableConstEntityContainerView entities) + { + spawnedEntitiesCount += entities.size(); + }; + m_manager->SpawnAllEntities(*m_ticket, {}, AZStd::move(callback)); + m_manager->ProcessQueue(); + + EXPECT_EQ(NumEntities, spawnedEntitiesCount); + } + + TEST_F(SpawnableEntitiesManagerTest, ListEntities_Call_AllEntitiesAreReported) + { + static constexpr size_t NumEntities = 4; + FillSpawnable(NumEntities); + + bool allValidEntityIds = true; + size_t spawnedEntitiesCount = 0; + auto callback = [&allValidEntityIds, &spawnedEntitiesCount] + (AzFramework::EntitySpawnTicket&, AzFramework::SpawnableConstEntityContainerView entities) + { + for (auto&& entity : entities) + { + allValidEntityIds = entity->GetId().IsValid() && allValidEntityIds; + } + spawnedEntitiesCount += entities.size(); + }; + + m_manager->SpawnAllEntities(*m_ticket); + m_manager->ListEntities(*m_ticket, AZStd::move(callback)); + m_manager->ProcessQueue(); + + EXPECT_TRUE(allValidEntityIds); + EXPECT_EQ(NumEntities, spawnedEntitiesCount); + } + + TEST_F(SpawnableEntitiesManagerTest, ListIndicesAndEntities_Call_AllEntitiesAreReportedAndIncrementByOne) + { + static constexpr size_t NumEntities = 4; + FillSpawnable(NumEntities); + + bool allValidEntityIds = true; + size_t spawnedEntitiesCount = 0; + auto callback = [&allValidEntityIds, &spawnedEntitiesCount] + (AzFramework::EntitySpawnTicket&, AzFramework::SpawnableConstIndexEntityContainerView entities) + { + for (auto&& indexEntityPair : entities) + { + // Since all entities are spawned a single time, the indices should be 0..NumEntities. + if (indexEntityPair.GetIndex() == spawnedEntitiesCount) + { + spawnedEntitiesCount++; + } + allValidEntityIds = indexEntityPair.GetEntity()->GetId().IsValid() && allValidEntityIds; + } + }; + + m_manager->SpawnAllEntities(*m_ticket); + m_manager->ListIndicesAndEntities(*m_ticket, AZStd::move(callback)); + m_manager->ProcessQueue(); + + EXPECT_TRUE(allValidEntityIds); + EXPECT_EQ(NumEntities, spawnedEntitiesCount); + } +} // namespace UnitTest diff --git a/Code/Framework/Tests/frameworktests_files.cmake b/Code/Framework/Tests/frameworktests_files.cmake index 197bcc9fce..3fc84eb788 100644 --- a/Code/Framework/Tests/frameworktests_files.cmake +++ b/Code/Framework/Tests/frameworktests_files.cmake @@ -11,6 +11,7 @@ set(FILES ../AzCore/Tests/Main.cpp + Spawnable/SpawnableEntitiesManagerTests.cpp ArchiveCompressionTests.cpp ArchiveTests.cpp BehaviorEntityTests.cpp