Additional list function added to Spawnables.

A new list function was added to the SpawnableEntitiesInterface that list entities together with the id of the entity in the spawnable that was used to create it.
This change also include the setup for testing the SpawnableEntitiesManager plus a few tests to cover the newly added functionality.
This commit is contained in:
AMZN-koppersr
2021-05-24 19:47:15 -07:00
parent f5bc191a55
commit a807c0b4d8
6 changed files with 428 additions and 2 deletions
@@ -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)
@@ -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<void(EntitySpawnTicket&)>;
using ReloadSpawnableCallback = AZStd::function<void(EntitySpawnTicket&, SpawnableConstEntityContainerView)>;
using ListEntitiesCallback = AZStd::function<void(EntitySpawnTicket&, SpawnableConstEntityContainerView)>;
using ListIndicesEntitiesCallback = AZStd::function<void(EntitySpawnTicket&, SpawnableConstIndexEntityContainerView)>;
using ClaimEntitiesCallback = AZStd::function<void(EntitySpawnTicket&, SpawnableEntityContainerView)>;
using BarrierCallback = AZStd::function<void(EntitySpawnTicket&)>;
@@ -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.
@@ -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<size_t> entityIndices,
EntityPreInsertionCallback preInsertionCallback, EntitySpawnCallback completionCallback)
{
AZ_Assert(ticket.IsValid(), "Ticket provided to SpawnEntities hasn't been initialized.");
SpawnEntitiesCommand queueEntry;
queueEntry.m_ticket = &ticket;
queueEntry.m_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> 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>(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<Ticket>(*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<Ticket>(*request.m_ticket);
@@ -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<SpawnAllEntitiesCommand, SpawnEntitiesCommand, DespawnAllEntitiesCommand, ReloadSpawnableCommand,
ListEntitiesCommand, ClaimEntitiesCommand, BarrierCommand, DestroyTicketCommand>;
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);
@@ -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 <AzCore/UnitTest/TestTypes.h>
#include <AzFramework/Application/Application.h>
#include <AzFramework/Spawnable/SpawnableAssetHandler.h>
#include <AzFramework/Spawnable/SpawnableEntitiesManager.h>
#include <AzTest/AzTest.h>
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<AzFramework::Spawnable>(m_spawnable, AZ::Data::AssetLoadBehavior::Default);
m_ticket = new AzFramework::EntitySpawnTicket(*m_spawnableAsset);
auto managerInterface = AzFramework::SpawnableEntitiesInterface::Get();
m_manager = azrtti_cast<AzFramework::SpawnableEntitiesManager*>(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<numElements; ++i)
{
entities.push_back(AZStd::make_unique<AZ::Entity>());
}
}
protected:
AZ::Data::Asset<AzFramework::Spawnable>* 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
@@ -11,6 +11,7 @@
set(FILES
../AzCore/Tests/Main.cpp
Spawnable/SpawnableEntitiesManagerTests.cpp
ArchiveCompressionTests.cpp
ArchiveTests.cpp
BehaviorEntityTests.cpp