Reintroduced spawning multiple instances of the same entity
The following was changed: - The remapper in AZ::IdUtils now has an additional argument to tell it what to do when it encounters the same source entity id. The original behavior of ignoring the new entity id and returning the first occurrence is the default. The alternative behavior is to store the last known entity id and return that instead. - Split the optional arguments for SpawnAllEntities and SpawnEntities. - SpawnEntities now has an option to continue with the entity mapping from a previous spawn call or to start with a fresh mapping. The latter is the default as the former will come at a performance cost since the mapping table has to be reconstructed. - Entities spawned using SpawnEntities and ReloadEntities now also get the correct entity mapping applied. - Added several new unit tests to cover most of the new functionality. - Fixed some places where the older API version was still called.
This commit is contained in:
@@ -28,7 +28,13 @@ namespace AZ
|
||||
{
|
||||
namespace IdUtils
|
||||
{
|
||||
template<typename IdType>
|
||||
/**
|
||||
* \param AllowDuplicates - If true allows the same id to be registered multiple time,
|
||||
with the newer value overwriting the stored value. If false, duplicates are not allowed and
|
||||
the first stored value is kept.The default is false.
|
||||
*/
|
||||
|
||||
template<typename IdType, bool AllowDuplicates = false>
|
||||
struct Remapper
|
||||
{
|
||||
/**
|
||||
@@ -138,14 +144,18 @@ namespace AZ
|
||||
* \param context - The serialize context for enumerating the @classPtr elements
|
||||
*/
|
||||
template<typename T, typename MapType>
|
||||
static void GenerateNewIdsAndFixRefs(T* object, MapType& newIdMap, AZ::SerializeContext* context = nullptr)
|
||||
static void GenerateNewIdsAndFixRefs(
|
||||
T* object, MapType& newIdMap, AZ::SerializeContext* context = nullptr)
|
||||
{
|
||||
if (!context)
|
||||
{
|
||||
AZ::ComponentApplicationBus::BroadcastResult(context, &AZ::ComponentApplicationRequests::GetSerializeContext);
|
||||
if (!context)
|
||||
{
|
||||
AZ_Error("Serialization", false, "No serialize context provided! Failed to get component application default serialize context! ComponentApp is not started or input serialize context should not be null!");
|
||||
AZ_Error(
|
||||
"Serialization", false,
|
||||
"No serialize context provided! Failed to get component application default serialize context! ComponentApp is "
|
||||
"not started or input serialize context should not be null!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -156,8 +166,16 @@ namespace AZ
|
||||
{
|
||||
if (idGenerator)
|
||||
{
|
||||
auto it = newIdMap.emplace(originalId, idGenerator());
|
||||
return it.first->second;
|
||||
if constexpr(AllowDuplicates)
|
||||
{
|
||||
auto it = newIdMap.insert_or_assign(originalId, idGenerator());
|
||||
return it.first->second;
|
||||
}
|
||||
else
|
||||
{
|
||||
auto it = newIdMap.emplace(originalId, idGenerator());
|
||||
return it.first->second;
|
||||
}
|
||||
}
|
||||
return originalId;
|
||||
}
|
||||
|
||||
@@ -30,8 +30,10 @@ namespace AZ
|
||||
bool m_isModifiedContainer;
|
||||
};
|
||||
|
||||
template<typename IdType>
|
||||
unsigned int Remapper<IdType>::RemapIds(void* classPtr, const AZ::Uuid& classUuid, const typename Remapper<IdType>::IdMapper& mapper, AZ::SerializeContext* context, bool replaceId)
|
||||
template<typename IdType, bool AllowDuplicates>
|
||||
unsigned int Remapper<IdType, AllowDuplicates>::RemapIds(
|
||||
void* classPtr, const AZ::Uuid& classUuid, const typename Remapper<IdType, AllowDuplicates>::IdMapper& mapper,
|
||||
AZ::SerializeContext* context, bool replaceId)
|
||||
{
|
||||
if (!context)
|
||||
{
|
||||
@@ -152,16 +154,18 @@ namespace AZ
|
||||
return replaced;
|
||||
}
|
||||
|
||||
template<typename IdType>
|
||||
unsigned int Remapper<IdType>::ReplaceIdsAndIdRefs(void* classPtr, const AZ::Uuid& classUuid, const IdMapper& mapper, AZ::SerializeContext* context /*= nullptr*/)
|
||||
template<typename IdType, bool AllowDuplicates>
|
||||
unsigned int Remapper<IdType, AllowDuplicates>::ReplaceIdsAndIdRefs(void* classPtr, const AZ::Uuid& classUuid, const IdMapper& mapper, AZ::SerializeContext* context /*= nullptr*/)
|
||||
{
|
||||
unsigned int replaced = RemapIds(classPtr, classUuid, mapper, context, true);
|
||||
replaced += RemapIds(classPtr, classUuid, mapper, context, false);
|
||||
return replaced;
|
||||
}
|
||||
|
||||
template<typename IdType>
|
||||
unsigned int Remapper<IdType>::RemapIdsAndIdRefs(void* classPtr, const AZ::Uuid& classUuid, const typename Remapper<IdType>::IdReplacer& mapper, AZ::SerializeContext* context)
|
||||
template<typename IdType, bool AllowDuplicates>
|
||||
unsigned int Remapper<IdType, AllowDuplicates>::RemapIdsAndIdRefs(
|
||||
void* classPtr, const AZ::Uuid& classUuid, const typename Remapper<IdType, AllowDuplicates>::IdReplacer& mapper,
|
||||
AZ::SerializeContext* context)
|
||||
{
|
||||
if (!context)
|
||||
{
|
||||
|
||||
@@ -172,7 +172,7 @@ namespace AzFramework
|
||||
using ClaimEntitiesCallback = AZStd::function<void(EntitySpawnTicket::Id, SpawnableEntityContainerView)>;
|
||||
using BarrierCallback = AZStd::function<void(EntitySpawnTicket::Id)>;
|
||||
|
||||
struct SpawnEntitiesOptionalArgs final
|
||||
struct SpawnAllEntitiesOptionalArgs final
|
||||
{
|
||||
//! Callback that's called after instances of entities have been created, but before they're spawned into the world. This
|
||||
//! gives the opportunity to modify the entities if needed such as injecting additional components or modifying components.
|
||||
@@ -186,6 +186,25 @@ namespace AzFramework
|
||||
SpawnablePriority m_priority { SpawnablePriority_Default };
|
||||
};
|
||||
|
||||
struct SpawnEntitiesOptionalArgs final
|
||||
{
|
||||
//! Callback that's called after instances of entities have been created, but before they're spawned into the world. This
|
||||
//! gives the opportunity to modify the entities if needed such as injecting additional components or modifying components.
|
||||
EntityPreInsertionCallback m_preInsertionCallback;
|
||||
//! 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.
|
||||
EntitySpawnCallback m_completionCallback;
|
||||
//! The Serialize Context used to clone entities with. If this is not provided the global Serialize Contetx will be used.
|
||||
AZ::SerializeContext* m_serializeContext{ nullptr };
|
||||
//! The priority at which this call will be executed.
|
||||
SpawnablePriority m_priority{ SpawnablePriority_Default };
|
||||
//! Entity references are resolved by referring to the last entity spawned from a template entity in the spawnable. If this
|
||||
//! is set to false entities from previous spawn calls are not taken into account. If set to true entity references may be
|
||||
//! resolved to a previously spawned entity. A lookup table has to be constructed when true, which may negatively impact
|
||||
//! performance, especially if a large number of entities are present on a ticket.
|
||||
bool m_referencePreviouslySpawnedEntities{ false };
|
||||
};
|
||||
|
||||
struct DespawnAllEntitiesOptionalArgs final
|
||||
{
|
||||
//! Callback that's called when spawning entities has completed. This can be called from a different thread than the one that
|
||||
@@ -250,7 +269,7 @@ namespace AzFramework
|
||||
//! Spawn instances of all entities in the spawnable.
|
||||
//! @param ticket Stores the results of the call. Use this ticket to spawn additional entities or to despawn them.
|
||||
//! @param optionalArgs Optional additional arguments, see SpawnAllEntitiesOptionalArgs
|
||||
virtual void SpawnAllEntities(EntitySpawnTicket& ticket, SpawnEntitiesOptionalArgs optionalArgs = {}) = 0;
|
||||
virtual void SpawnAllEntities(EntitySpawnTicket& ticket, SpawnAllEntitiesOptionalArgs optionalArgs = {}) = 0;
|
||||
//! Spawn instances of some entities in the spawnable.
|
||||
//! @param ticket Stores the results of the call. Use this ticket to spawn additional entities or to despawn them.
|
||||
//! @param priority The priority at which this call will be executed.
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace AzFramework
|
||||
}
|
||||
}
|
||||
|
||||
void SpawnableEntitiesManager::SpawnAllEntities(EntitySpawnTicket& ticket, SpawnEntitiesOptionalArgs optionalArgs)
|
||||
void SpawnableEntitiesManager::SpawnAllEntities(EntitySpawnTicket& ticket, SpawnAllEntitiesOptionalArgs optionalArgs)
|
||||
{
|
||||
AZ_Assert(ticket.IsValid(), "Ticket provided to SpawnAllEntities hasn't been initialized.");
|
||||
|
||||
@@ -75,6 +75,7 @@ namespace AzFramework
|
||||
optionalArgs.m_serializeContext == nullptr ? m_defaultSerializeContext : optionalArgs.m_serializeContext;
|
||||
queueEntry.m_completionCallback = AZStd::move(optionalArgs.m_completionCallback);
|
||||
queueEntry.m_preInsertionCallback = AZStd::move(optionalArgs.m_preInsertionCallback);
|
||||
queueEntry.m_referencePreviouslySpawnedEntities = optionalArgs.m_referencePreviouslySpawnedEntities;
|
||||
QueueRequest(ticket, optionalArgs.m_priority, AZStd::move(queueEntry));
|
||||
}
|
||||
|
||||
@@ -256,21 +257,11 @@ namespace AzFramework
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
AZ::Entity* SpawnableEntitiesManager::CloneSingleEntity(const AZ::Entity& entityTemplate,
|
||||
EntityIdMap& templateToCloneEntityIdMap, AZ::SerializeContext& serializeContext)
|
||||
EntityIdMap& templateToCloneMap, AZ::SerializeContext& serializeContext)
|
||||
{
|
||||
return AZ::IdUtils::Remapper<AZ::EntityId>::CloneObjectAndGenerateNewIdsAndFixRefs(
|
||||
&entityTemplate, templateToCloneEntityIdMap, &serializeContext);
|
||||
return AZ::IdUtils::Remapper<AZ::EntityId, true>::CloneObjectAndGenerateNewIdsAndFixRefs(
|
||||
&entityTemplate, templateToCloneMap, &serializeContext);
|
||||
}
|
||||
|
||||
bool SpawnableEntitiesManager::ProcessRequest(SpawnAllEntitiesCommand& request)
|
||||
@@ -297,13 +288,9 @@ namespace AzFramework
|
||||
spawnedEntityIndices.reserve(spawnedEntityIndices.size() + entitiesToSpawnSize);
|
||||
templateToCloneEntityIdMap.reserve(entitiesToSpawnSize);
|
||||
|
||||
// Mark all indices as spawned
|
||||
for (size_t i = 0; i < entitiesToSpawnSize; ++i)
|
||||
{
|
||||
const AZ::Entity& entityTemplate = *entitiesToSpawn[i];
|
||||
|
||||
AZ::Entity* clone = CloneSingleEntity(entityTemplate, templateToCloneEntityIdMap, *request.m_serializeContext);
|
||||
|
||||
AZ::Entity* clone = CloneSingleEntity(*entitiesToSpawn[i], templateToCloneEntityIdMap, *request.m_serializeContext);
|
||||
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
|
||||
|
||||
spawnedEntities.emplace_back(clone);
|
||||
@@ -311,16 +298,8 @@ namespace AzFramework
|
||||
}
|
||||
|
||||
// loadAll is true if every entity has been spawned only once
|
||||
if (spawnedEntities.size() == entitiesToSpawnSize)
|
||||
{
|
||||
ticket.m_loadAll = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Case where there were already spawns from a previous request
|
||||
ticket.m_loadAll = false;
|
||||
}
|
||||
|
||||
ticket.m_loadAll = (spawnedEntities.size() == entitiesToSpawnSize);
|
||||
|
||||
// Let other systems know about newly spawned entities for any pre-processing before adding to the scene/game context.
|
||||
if (request.m_preInsertionCallback)
|
||||
{
|
||||
@@ -329,11 +308,10 @@ namespace AzFramework
|
||||
}
|
||||
|
||||
// Add to the game context, now the entities are active
|
||||
AZStd::for_each(ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end(),
|
||||
[](AZ::Entity* entity)
|
||||
for (auto it = ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount; it != ticket.m_spawnedEntities.end(); ++it)
|
||||
{
|
||||
GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, entity);
|
||||
});
|
||||
GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, *it);
|
||||
}
|
||||
|
||||
// Let other systems know about newly spawned entities for any post-processing after adding to the scene/game context.
|
||||
if (request.m_completionCallback)
|
||||
@@ -360,14 +338,34 @@ namespace AzFramework
|
||||
{
|
||||
AZStd::vector<AZ::Entity*>& spawnedEntities = ticket.m_spawnedEntities;
|
||||
AZStd::vector<size_t>& spawnedEntityIndices = ticket.m_spawnedEntityIndices;
|
||||
AZ_Assert(
|
||||
spawnedEntities.size() == spawnedEntityIndices.size(),
|
||||
"The indices for the spawned entities has gone out of sync with the entities.");
|
||||
|
||||
// Keep track how many entities there were in the array initially
|
||||
// Keep track of how many entities there were in the array initially
|
||||
size_t spawnedEntitiesInitialCount = spawnedEntities.size();
|
||||
|
||||
// These are 'template' entities we'll be cloning from
|
||||
const Spawnable::EntityList& entitiesToSpawn = ticket.m_spawnable->GetEntities();
|
||||
size_t entitiesToSpawnSize = request.m_entityIndices.size();
|
||||
|
||||
// Reconstruct the template to entity mapping.
|
||||
EntityIdMap templateToCloneEntityIdMap;
|
||||
if (!request.m_referencePreviouslySpawnedEntities)
|
||||
{
|
||||
templateToCloneEntityIdMap.reserve(entitiesToSpawnSize);
|
||||
}
|
||||
else
|
||||
{
|
||||
templateToCloneEntityIdMap.reserve(spawnedEntitiesInitialCount + entitiesToSpawnSize);
|
||||
SpawnableConstIndexEntityContainerView indexEntityView(
|
||||
spawnedEntities.begin(), spawnedEntityIndices.begin(), spawnedEntities.size());
|
||||
for (auto& entry : indexEntityView)
|
||||
{
|
||||
templateToCloneEntityIdMap.insert_or_assign(entitiesToSpawn[entry.GetIndex()]->GetId(), entry.GetEntity()->GetId());
|
||||
}
|
||||
}
|
||||
|
||||
spawnedEntities.reserve(spawnedEntities.size() + entitiesToSpawnSize);
|
||||
spawnedEntityIndices.reserve(spawnedEntityIndices.size() + entitiesToSpawnSize);
|
||||
|
||||
@@ -375,15 +373,11 @@ namespace AzFramework
|
||||
{
|
||||
if (index < entitiesToSpawn.size())
|
||||
{
|
||||
const AZ::Entity& entityTemplate = *entitiesToSpawn[index];
|
||||
|
||||
AZ::Entity* clone = request.m_serializeContext->CloneObject(&entityTemplate);
|
||||
AZ::Entity* clone = CloneSingleEntity(*entitiesToSpawn[index], templateToCloneEntityIdMap, *request.m_serializeContext);
|
||||
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
|
||||
clone->SetId(AZ::Entity::MakeId());
|
||||
|
||||
spawnedEntities.push_back(clone);
|
||||
spawnedEntityIndices.push_back(index);
|
||||
|
||||
}
|
||||
}
|
||||
ticket.m_loadAll = false;
|
||||
@@ -396,11 +390,10 @@ namespace AzFramework
|
||||
}
|
||||
|
||||
// Add to the game context, now the entities are active
|
||||
AZStd::for_each(ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end(),
|
||||
[](AZ::Entity* entity)
|
||||
for (auto it = ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount; it != ticket.m_spawnedEntities.end(); ++it)
|
||||
{
|
||||
GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, entity);
|
||||
});
|
||||
GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, *it);
|
||||
}
|
||||
|
||||
if (request.m_completionCallback)
|
||||
{
|
||||
@@ -475,39 +468,43 @@ namespace AzFramework
|
||||
// Rebuild the list of entities.
|
||||
ticket.m_spawnedEntities.clear();
|
||||
const Spawnable::EntityList& entities = request.m_spawnable->GetEntities();
|
||||
|
||||
// Map keeps track of ids from template (spawnable) to clone (instance)
|
||||
// Allowing patch ups of fields referring to entityIds outside of a given entity
|
||||
EntityIdMap templateToCloneEntityIdMap;
|
||||
|
||||
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.
|
||||
// to spawn every entity, simply start over.
|
||||
ticket.m_spawnedEntityIndices.clear();
|
||||
|
||||
size_t entitiesToSpawnSize = entities.size();
|
||||
|
||||
// Map keeps track of ids from template (spawnable) to clone (instance)
|
||||
// Allowing patch ups of fields referring to entityIds outside of a given entity
|
||||
EntityIdMap templateToCloneEntityIdMap;
|
||||
templateToCloneEntityIdMap.reserve(entitiesToSpawnSize);
|
||||
|
||||
// Mark all indices as spawned
|
||||
for (size_t i = 0; i < entitiesToSpawnSize; ++i)
|
||||
{
|
||||
const AZ::Entity& entityTemplate = *entities[i];
|
||||
|
||||
AZ::Entity* clone = CloneSingleEntity(entityTemplate, templateToCloneEntityIdMap, *request.m_serializeContext);
|
||||
|
||||
AZ::Entity* clone = CloneSingleEntity(*entities[i], templateToCloneEntityIdMap, *request.m_serializeContext);
|
||||
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
|
||||
|
||||
ticket.m_spawnedEntities.emplace_back(clone);
|
||||
ticket.m_spawnedEntities.push_back(clone);
|
||||
ticket.m_spawnedEntityIndices.push_back(i);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
size_t entitiesSize = entities.size();
|
||||
templateToCloneEntityIdMap.reserve(entitiesSize);
|
||||
for (size_t index : ticket.m_spawnedEntityIndices)
|
||||
{
|
||||
ticket.m_spawnedEntities.push_back(
|
||||
index < entitiesSize ? SpawnSingleEntity(*entities[index], *request.m_serializeContext) : nullptr);
|
||||
// It's possible for the new spawnable to have a different number of entities, so guard against this.
|
||||
// It's also possible that the entities have moved within the spawnable to a new index. This can't be
|
||||
// detected and will result in the incorrect entities being spawned.
|
||||
if (index < entitiesSize)
|
||||
{
|
||||
AZ::Entity* clone = CloneSingleEntity(*entities[index], templateToCloneEntityIdMap, *request.m_serializeContext);
|
||||
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
|
||||
ticket.m_spawnedEntities.push_back(clone);
|
||||
}
|
||||
}
|
||||
}
|
||||
ticket.m_spawnable = AZStd::move(request.m_spawnable);
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace AzFramework
|
||||
AZ_CLASS_ALLOCATOR(SpawnableEntitiesManager, AZ::SystemAllocator, 0);
|
||||
|
||||
using EntityIdMap = AZStd::unordered_map<AZ::EntityId, AZ::EntityId>;
|
||||
|
||||
|
||||
enum class CommandQueueStatus : bool
|
||||
{
|
||||
HasCommandsLeft,
|
||||
@@ -57,7 +57,7 @@ namespace AzFramework
|
||||
// The following functions are thread safe
|
||||
//
|
||||
|
||||
void SpawnAllEntities(EntitySpawnTicket& ticket, SpawnEntitiesOptionalArgs optionalArgs = {}) override;
|
||||
void SpawnAllEntities(EntitySpawnTicket& ticket, SpawnAllEntitiesOptionalArgs optionalArgs = {}) override;
|
||||
void SpawnEntities(
|
||||
EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices, SpawnEntitiesOptionalArgs optionalArgs = {}) override;
|
||||
void DespawnAllEntities(EntitySpawnTicket& ticket, DespawnAllEntitiesOptionalArgs optionalArgs = {}) override;
|
||||
@@ -114,6 +114,7 @@ namespace AzFramework
|
||||
Ticket* m_ticket;
|
||||
EntitySpawnTicket::Id m_ticketId;
|
||||
uint32_t m_requestId;
|
||||
bool m_referencePreviouslySpawnedEntities;
|
||||
};
|
||||
struct DespawnAllEntitiesCommand
|
||||
{
|
||||
@@ -183,12 +184,9 @@ namespace AzFramework
|
||||
|
||||
CommandQueueStatus ProcessQueue(Queue& queue);
|
||||
|
||||
AZ::Entity* SpawnSingleEntity(const AZ::Entity& entityTemplate,
|
||||
AZ::SerializeContext& serializeContext);
|
||||
|
||||
AZ::Entity* CloneSingleEntity(const AZ::Entity& entityTemplate,
|
||||
EntityIdMap& templateToCloneEntityIdMap, AZ::SerializeContext& serializeContext);
|
||||
|
||||
AZ::Entity* CloneSingleEntity(
|
||||
const AZ::Entity& entityTemplate, EntityIdMap& templateToCloneMap, AZ::SerializeContext& serializeContext);
|
||||
|
||||
bool ProcessRequest(SpawnAllEntitiesCommand& request);
|
||||
bool ProcessRequest(SpawnEntitiesCommand& request);
|
||||
bool ProcessRequest(DespawnAllEntitiesCommand& request);
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <AzFramework/Application/Application.h>
|
||||
#include <AzFramework/Spawnable/SpawnableAssetHandler.h>
|
||||
#include <AzFramework/Spawnable/SpawnableEntitiesManager.h>
|
||||
#include <AzFramework/Components/TransformComponent.h>
|
||||
#include <AzTest/AzTest.h>
|
||||
|
||||
namespace UnitTest
|
||||
@@ -81,6 +82,42 @@ namespace UnitTest
|
||||
}
|
||||
}
|
||||
|
||||
void CreateRecursiveHierarchy()
|
||||
{
|
||||
AzFramework::Spawnable::EntityList& entities = m_spawnable->GetEntities();
|
||||
size_t numElements = entities.size();
|
||||
AZ::EntityId parent;
|
||||
for (size_t i=0; i<numElements; ++i)
|
||||
{
|
||||
AZStd::unique_ptr<AZ::Entity>& entity = entities[i];
|
||||
auto component = entity->CreateComponent<AzFramework::TransformComponent>();
|
||||
if (i > 0)
|
||||
{
|
||||
component->SetParent(parent);
|
||||
}
|
||||
parent = entity->GetId();
|
||||
}
|
||||
}
|
||||
|
||||
void CreateSingleParent()
|
||||
{
|
||||
AzFramework::Spawnable::EntityList& entities = m_spawnable->GetEntities();
|
||||
size_t numElements = entities.size();
|
||||
if (numElements > 0)
|
||||
{
|
||||
AZ::EntityId parent = entities[0]->GetId();
|
||||
for (size_t i = 0; i < numElements; ++i)
|
||||
{
|
||||
AZStd::unique_ptr<AZ::Entity>& entity = entities[i];
|
||||
auto component = entity->CreateComponent<AzFramework::TransformComponent>();
|
||||
if (i > 0)
|
||||
{
|
||||
component->SetParent(parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
AZ::Data::Asset<AzFramework::Spawnable>* m_spawnableAsset { nullptr };
|
||||
AzFramework::SpawnableEntitiesManager* m_manager { nullptr };
|
||||
@@ -104,7 +141,7 @@ namespace UnitTest
|
||||
{
|
||||
spawnedEntitiesCount += entities.size();
|
||||
};
|
||||
AzFramework::SpawnEntitiesOptionalArgs optionalArgs;
|
||||
AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs;
|
||||
optionalArgs.m_completionCallback = AZStd::move(callback);
|
||||
m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs));
|
||||
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
|
||||
@@ -112,6 +149,37 @@ namespace UnitTest
|
||||
EXPECT_EQ(NumEntities, spawnedEntitiesCount);
|
||||
}
|
||||
|
||||
TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_SetParentOnSpawnedEntities_LineageIsPreserved)
|
||||
{
|
||||
static constexpr size_t NumEntities = 4;
|
||||
FillSpawnable(NumEntities);
|
||||
CreateRecursiveHierarchy();
|
||||
|
||||
auto callback = [](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
|
||||
{
|
||||
AZ::EntityId parentId;
|
||||
bool isFirst = true;
|
||||
for (const AZ::Entity* entity : entities)
|
||||
{
|
||||
if (!isFirst)
|
||||
{
|
||||
auto transform = entity->GetTransform();
|
||||
ASSERT_NE(nullptr, transform);
|
||||
EXPECT_EQ(parentId, transform->GetParentId());
|
||||
}
|
||||
else
|
||||
{
|
||||
isFirst = false;
|
||||
}
|
||||
parentId = entity->GetId();
|
||||
}
|
||||
};
|
||||
AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs;
|
||||
optionalArgs.m_completionCallback = AZStd::move(callback);
|
||||
m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs));
|
||||
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
|
||||
}
|
||||
|
||||
TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_DeleteTicketBeforeCall_NoCrash)
|
||||
{
|
||||
{
|
||||
@@ -126,6 +194,170 @@ namespace UnitTest
|
||||
// SpawnEntities
|
||||
//
|
||||
|
||||
TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_Call_AllEntitiesSpawned)
|
||||
{
|
||||
static constexpr size_t NumEntities = 4;
|
||||
FillSpawnable(NumEntities);
|
||||
|
||||
AZStd::vector<size_t> indices = { 0, 2, 3, 1 };
|
||||
|
||||
size_t spawnedEntitiesCount = 0;
|
||||
auto callback = [&spawnedEntitiesCount](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
|
||||
{
|
||||
spawnedEntitiesCount += entities.size();
|
||||
};
|
||||
AzFramework::SpawnEntitiesOptionalArgs optionalArgs;
|
||||
optionalArgs.m_completionCallback = AZStd::move(callback);
|
||||
m_manager->SpawnEntities(*m_ticket, AZStd::move(indices), AZStd::move(optionalArgs));
|
||||
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
|
||||
|
||||
EXPECT_EQ(NumEntities, spawnedEntitiesCount);
|
||||
}
|
||||
|
||||
TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_SpawnTheSameEntity_AllEntitiesSpawned)
|
||||
{
|
||||
static constexpr size_t NumEntities = 1;
|
||||
FillSpawnable(NumEntities);
|
||||
|
||||
AZStd::vector<size_t> indices = { 0, 0 };
|
||||
|
||||
size_t spawnedEntitiesCount = 0;
|
||||
auto callback =
|
||||
[&spawnedEntitiesCount](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
|
||||
{
|
||||
spawnedEntitiesCount += entities.size();
|
||||
};
|
||||
AzFramework::SpawnEntitiesOptionalArgs optionalArgs;
|
||||
optionalArgs.m_completionCallback = AZStd::move(callback);
|
||||
m_manager->SpawnEntities(*m_ticket, AZStd::move(indices), AZStd::move(optionalArgs));
|
||||
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
|
||||
|
||||
EXPECT_EQ(NumEntities * 2, spawnedEntitiesCount);
|
||||
}
|
||||
|
||||
TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_MultipleSpawns_AllEntitiesSpawned)
|
||||
{
|
||||
static constexpr size_t NumEntities = 4;
|
||||
FillSpawnable(NumEntities);
|
||||
|
||||
AZStd::vector<size_t> indices = { 0, 2, 3, 1 };
|
||||
|
||||
size_t spawnedEntitiesCount = 0;
|
||||
auto callback =
|
||||
[&spawnedEntitiesCount](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
|
||||
{
|
||||
spawnedEntitiesCount += entities.size();
|
||||
};
|
||||
AzFramework::SpawnEntitiesOptionalArgs optionalArgs;
|
||||
optionalArgs.m_completionCallback = AZStd::move(callback);
|
||||
m_manager->SpawnEntities(*m_ticket, indices, optionalArgs);
|
||||
m_manager->SpawnEntities(*m_ticket, AZStd::move(indices), AZStd::move(optionalArgs));
|
||||
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
|
||||
|
||||
EXPECT_EQ(NumEntities * 2, spawnedEntitiesCount);
|
||||
}
|
||||
|
||||
TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_ReferencesAreRemappedForNewBatch_AllPointToLatestParent)
|
||||
{
|
||||
static constexpr size_t NumEntities = 4;
|
||||
FillSpawnable(NumEntities);
|
||||
CreateSingleParent();
|
||||
|
||||
AZStd::vector<size_t> indices = { 0, 1, 2, 3 };
|
||||
AZStd::vector<AZ::EntityId> parents;
|
||||
|
||||
auto callback = [&parents](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
|
||||
{
|
||||
AZ::EntityId parent = (*entities.begin())->GetId();
|
||||
parents.push_back(parent);
|
||||
auto it = entities.begin();
|
||||
++it; // Skip the first as that is the parent.
|
||||
for (; it != entities.end(); ++it)
|
||||
{
|
||||
AZ::TransformInterface* transform = (*it)->GetTransform();
|
||||
ASSERT_NE(nullptr, transform);
|
||||
ASSERT_EQ(parent, transform->GetParentId());
|
||||
}
|
||||
};
|
||||
AzFramework::SpawnEntitiesOptionalArgs optionalArgs;
|
||||
optionalArgs.m_completionCallback = AZStd::move(callback);
|
||||
optionalArgs.m_referencePreviouslySpawnedEntities = false;
|
||||
m_manager->SpawnEntities(*m_ticket, indices, optionalArgs);
|
||||
m_manager->SpawnEntities(*m_ticket, AZStd::move(indices), AZStd::move(optionalArgs));
|
||||
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
|
||||
|
||||
EXPECT_NE(parents[0], parents[1]);
|
||||
}
|
||||
|
||||
TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_ReferencesAreRemappedForContinuedBatch_AllPointToLatestParent)
|
||||
{
|
||||
static constexpr size_t NumEntities = 4;
|
||||
FillSpawnable(NumEntities);
|
||||
CreateSingleParent();
|
||||
|
||||
AZStd::vector<size_t> indices = { 0, 1, 2, 3 };
|
||||
AZStd::vector<AZ::EntityId> parents;
|
||||
|
||||
auto callback =
|
||||
[&parents](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
|
||||
{
|
||||
AZ::EntityId parent = (*entities.begin())->GetId();
|
||||
parents.push_back(parent);
|
||||
auto it = entities.begin();
|
||||
++it; // Skip the first as that is the parent.
|
||||
for (; it!=entities.end(); ++it)
|
||||
{
|
||||
AZ::TransformInterface* transform = (*it)->GetTransform();
|
||||
ASSERT_NE(nullptr, transform);
|
||||
ASSERT_EQ(parent, transform->GetParentId());
|
||||
}
|
||||
};
|
||||
AzFramework::SpawnEntitiesOptionalArgs optionalArgs;
|
||||
optionalArgs.m_completionCallback = AZStd::move(callback);
|
||||
optionalArgs.m_referencePreviouslySpawnedEntities = true;
|
||||
m_manager->SpawnEntities(*m_ticket, indices, optionalArgs);
|
||||
m_manager->SpawnEntities(*m_ticket, AZStd::move(indices), AZStd::move(optionalArgs));
|
||||
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
|
||||
|
||||
EXPECT_NE(parents[0], parents[1]);
|
||||
}
|
||||
|
||||
TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_ReferencesAreRemappedAcrossBatches_AllPointToLatestParent)
|
||||
{
|
||||
FillSpawnable(4);
|
||||
CreateSingleParent();
|
||||
|
||||
// Spawn a regular batch but with two parents and store the id of the last entity. This will the parent for the next batch.
|
||||
AZ::EntityId parent;
|
||||
auto getParent = [&parent](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
|
||||
{
|
||||
ASSERT_NE(entities.begin(), entities.end());
|
||||
parent = (*AZStd::prev(entities.end()))->GetId();
|
||||
};
|
||||
|
||||
AzFramework::SpawnEntitiesOptionalArgs optionalArgsFirstBatch;
|
||||
optionalArgsFirstBatch.m_completionCallback = AZStd::move(getParent);
|
||||
optionalArgsFirstBatch.m_referencePreviouslySpawnedEntities = true;
|
||||
m_manager->SpawnEntities(*m_ticket, {0, 1, 2, 3, 0}, AZStd::move(optionalArgsFirstBatch));
|
||||
|
||||
// Next, spawn all the entities that have a reference to the parent that was just stored.
|
||||
auto parentCheck = [&parent](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
|
||||
{
|
||||
for (auto& it : entities)
|
||||
{
|
||||
AZ::TransformInterface* transform = it->GetTransform();
|
||||
ASSERT_NE(nullptr, transform);
|
||||
ASSERT_EQ(parent, transform->GetParentId());
|
||||
}
|
||||
};
|
||||
AzFramework::SpawnEntitiesOptionalArgs optionalArgsSecondBatch;
|
||||
optionalArgsSecondBatch.m_completionCallback = AZStd::move(parentCheck);
|
||||
optionalArgsSecondBatch.m_referencePreviouslySpawnedEntities = true;
|
||||
m_manager->SpawnEntities(*m_ticket, {1, 2, 3}, AZStd::move(optionalArgsSecondBatch));
|
||||
|
||||
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
|
||||
}
|
||||
|
||||
TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_DeleteTicketBeforeCall_NoCrash)
|
||||
{
|
||||
{
|
||||
@@ -186,7 +418,7 @@ namespace UnitTest
|
||||
};
|
||||
|
||||
m_manager->SpawnAllEntities(*m_ticket);
|
||||
m_manager->ListEntities(*m_ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback));
|
||||
m_manager->ListEntities(*m_ticket, AZStd::move(callback));
|
||||
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
|
||||
|
||||
EXPECT_TRUE(allValidEntityIds);
|
||||
@@ -199,7 +431,7 @@ namespace UnitTest
|
||||
|
||||
{
|
||||
AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset);
|
||||
m_manager->ListEntities(ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback));
|
||||
m_manager->ListEntities(ticket, AZStd::move(callback));
|
||||
}
|
||||
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
|
||||
}
|
||||
@@ -231,7 +463,7 @@ namespace UnitTest
|
||||
};
|
||||
|
||||
m_manager->SpawnAllEntities(*m_ticket);
|
||||
m_manager->ListIndicesAndEntities(*m_ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback));
|
||||
m_manager->ListIndicesAndEntities(*m_ticket, AZStd::move(callback));
|
||||
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
|
||||
|
||||
EXPECT_TRUE(allValidEntityIds);
|
||||
@@ -244,7 +476,7 @@ namespace UnitTest
|
||||
|
||||
{
|
||||
AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset);
|
||||
m_manager->ListIndicesAndEntities(ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback));
|
||||
m_manager->ListIndicesAndEntities(ticket, AZStd::move(callback));
|
||||
}
|
||||
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
|
||||
}
|
||||
@@ -260,7 +492,7 @@ namespace UnitTest
|
||||
|
||||
{
|
||||
AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset);
|
||||
m_manager->ClaimEntities(ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback));
|
||||
m_manager->ClaimEntities(ticket, AZStd::move(callback));
|
||||
}
|
||||
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
|
||||
}
|
||||
@@ -276,7 +508,7 @@ namespace UnitTest
|
||||
|
||||
{
|
||||
AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset);
|
||||
m_manager->Barrier(ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback));
|
||||
m_manager->Barrier(ticket, AZStd::move(callback));
|
||||
}
|
||||
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
|
||||
}
|
||||
@@ -307,12 +539,12 @@ namespace UnitTest
|
||||
defaultPriorityCallId = callCounter++;
|
||||
};
|
||||
|
||||
AzFramework::SpawnEntitiesOptionalArgs optionalArgs;
|
||||
AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs;
|
||||
optionalArgs.m_completionCallback = AZStd::move(defaultCallback);
|
||||
optionalArgs.m_priority = AzFramework::SpawnablePriority_Default;
|
||||
m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs));
|
||||
|
||||
AzFramework::SpawnEntitiesOptionalArgs highPriortyOptionalArgs;
|
||||
AzFramework::SpawnAllEntitiesOptionalArgs highPriortyOptionalArgs;
|
||||
highPriortyOptionalArgs.m_completionCallback = AZStd::move(highCallback);
|
||||
highPriortyOptionalArgs.m_priority = AzFramework::SpawnablePriority_High;
|
||||
m_manager->SpawnAllEntities(highPriorityTicket, AZStd::move(highPriortyOptionalArgs));
|
||||
@@ -343,12 +575,12 @@ namespace UnitTest
|
||||
defaultPriorityCallId = callCounter++;
|
||||
};
|
||||
|
||||
AzFramework::SpawnEntitiesOptionalArgs optionalArgs;
|
||||
AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs;
|
||||
optionalArgs.m_completionCallback = AZStd::move(defaultCallback);
|
||||
optionalArgs.m_priority = AzFramework::SpawnablePriority_Default;
|
||||
m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs));
|
||||
|
||||
AzFramework::SpawnEntitiesOptionalArgs highPriortyOptionalArgs;
|
||||
AzFramework::SpawnAllEntitiesOptionalArgs highPriortyOptionalArgs;
|
||||
highPriortyOptionalArgs.m_completionCallback = AZStd::move(highCallback);
|
||||
highPriortyOptionalArgs.m_priority = AzFramework::SpawnablePriority_High;
|
||||
m_manager->SpawnAllEntities(*m_ticket, AZStd::move(highPriortyOptionalArgs));
|
||||
|
||||
@@ -81,8 +81,10 @@ namespace Multiplayer
|
||||
};
|
||||
|
||||
m_netSpawnTicket = AzFramework::EntitySpawnTicket(m_networkSpawnableAsset);
|
||||
AzFramework::SpawnEntitiesOptionalArgs optionalArgs;
|
||||
optionalArgs.m_preInsertionCallback = AZStd::move(preInsertionCallback);
|
||||
AzFramework::SpawnableEntitiesInterface::Get()->SpawnEntities(
|
||||
m_netSpawnTicket, AzFramework::SpawnablePriority_Default, { m_netEntityIndex }, preInsertionCallback);
|
||||
m_netSpawnTicket, { m_netEntityIndex }, AZStd::move(optionalArgs));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +92,7 @@ namespace Multiplayer
|
||||
{
|
||||
if(m_netSpawnTicket.IsValid())
|
||||
{
|
||||
AzFramework::SpawnableEntitiesInterface::Get()->DespawnAllEntities(m_netSpawnTicket, AzFramework::SpawnablePriority_Default);
|
||||
AzFramework::SpawnableEntitiesInterface::Get()->DespawnAllEntities(m_netSpawnTicket);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ namespace ScriptCanvas::Nodeables::Spawning
|
||||
m_spawnBatchSizes.push_back(view.size());
|
||||
};
|
||||
|
||||
AzFramework::SpawnEntitiesOptionalArgs optionalArgs;
|
||||
AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs;
|
||||
optionalArgs.m_preInsertionCallback = AZStd::move(preSpawnCB);
|
||||
optionalArgs.m_completionCallback = AZStd::move(spawnCompleteCB);
|
||||
AzFramework::SpawnableEntitiesInterface::Get()->SpawnAllEntities(m_spawnTicket, AZStd::move(optionalArgs));
|
||||
|
||||
Reference in New Issue
Block a user