diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h index 95d0b9e3a7..ac66288ff2 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h @@ -84,6 +84,7 @@ namespace AzFramework }; using EntitySpawnCallback = AZStd::function; + using EntityPreInsertionCallback = AZStd::function; using EntityDespawnCallback = AZStd::function; using ReloadSpawnableCallback = AZStd::function; using ListEntitiesCallback = AZStd::function; @@ -110,7 +111,8 @@ namespace AzFramework //! @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; + virtual void SpawnAllEntities(EntitySpawnTicket& ticket, EntityPreInsertionCallback preInsertionCallback = {}, + 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. @@ -118,7 +120,7 @@ namespace AzFramework //! 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 entityIndices, - EntitySpawnCallback completionCallback = {}) = 0; + EntityPreInsertionCallback preInsertionCallback = {}, 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 diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp index 3ab004b516..6799ea9353 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp @@ -14,17 +14,20 @@ #include #include #include +#include #include #include #include namespace AzFramework { - void SpawnableEntitiesManager::SpawnAllEntities(EntitySpawnTicket& ticket, EntitySpawnCallback completionCallback) + void SpawnableEntitiesManager::SpawnAllEntities(EntitySpawnTicket& ticket, EntityPreInsertionCallback preInsertionCallback, + EntitySpawnCallback completionCallback) { SpawnAllEntitiesCommand queueEntry; queueEntry.m_ticket = &ticket; queueEntry.m_completionCallback = AZStd::move(completionCallback); + queueEntry.m_preInsertionCallback = AZStd::move(preInsertionCallback); { AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex); queueEntry.m_ticketId = GetTicketPayload(ticket).m_nextTicketId++; @@ -32,13 +35,15 @@ namespace AzFramework } } - void SpawnableEntitiesManager::SpawnEntities(EntitySpawnTicket& ticket, AZStd::vector entityIndices, - EntitySpawnCallback completionCallback) + void SpawnableEntitiesManager::SpawnEntities( + EntitySpawnTicket& ticket, AZStd::vector entityIndices, + EntityPreInsertionCallback preInsertionCallback, EntitySpawnCallback completionCallback) { SpawnEntitiesCommand queueEntry; queueEntry.m_ticket = &ticket; queueEntry.m_entityIndices = AZStd::move(entityIndices); queueEntry.m_completionCallback = AZStd::move(completionCallback); + queueEntry.m_preInsertionCallback = AZStd::move(preInsertionCallback); { AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex); queueEntry.m_ticketId = GetTicketPayload(ticket).m_nextTicketId++; @@ -205,6 +210,7 @@ namespace AzFramework 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; } @@ -214,23 +220,79 @@ namespace AzFramework Ticket& ticket = GetTicketPayload(*request.m_ticket); if (ticket.m_spawnable.IsReady() && request.m_ticketId == ticket.m_currentTicketId) { - size_t spawnedEntitiesCount = ticket.m_spawnedEntities.size(); + AZStd::vector& spawnedEntities = ticket.m_spawnedEntities; + AZStd::vector& spawnedEntityIndices = ticket.m_spawnedEntityIndices; - 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); + // Keep track how many entities there were in the array initially + size_t spawnedEntitiesInitialCount = spawnedEntities.size(); - for(size_t i=0; iGetEntities(); + size_t entitiesToSpawnSize = entitiesToSpawn.size(); + + // Reserve buffers + spawnedEntities.reserve(spawnedEntities.size() + entitiesToSpawnSize); + ticket.m_spawnedEntityIndices.reserve(ticket.m_spawnedEntityIndices.size() + entitiesToSpawnSize); + + // TEMP: To be replaced by IdUtils::Remapper + using EntityIdMap = AZStd::unordered_map; + EntityIdMap templateToCloneIdMap; + // \TEMP + + // Clone the entities from Spawnable + for (size_t i = 0; i < entitiesToSpawnSize; ++i) { - ticket.m_spawnedEntities.push_back(SpawnSingleEntity(*entities[i], serializeContext)); - ticket.m_spawnedEntityIndices.push_back(i); + const AZ::Entity& entityTemplate = *entitiesToSpawn[i]; + + AZ::Entity* clone = serializeContext.CloneObject(&entityTemplate); + AZ_Assert(clone != nullptr, "Failed to clone spawnable entity."); + clone->SetId(AZ::Entity::MakeId()); + + spawnedEntities.push_back(clone); + spawnedEntityIndices.push_back(i); + + // TEMP: To be replaced by IdUtils::Remapper + templateToCloneIdMap[entityTemplate.GetId()] = clone->GetId(); + + // Update TransformComponent parent Id. It is guaranteed for the entities array to be sorted from parent->child here. + auto* transformComponent = clone->FindComponent(); + AZ::EntityId parentId = transformComponent->GetParentId(); + if (parentId.IsValid()) + { + auto it = templateToCloneIdMap.find(parentId); + if (it != templateToCloneIdMap.end()) + { + transformComponent->SetParentRelative(it->second); + } + else + { + AZ_Warning( + "SpawnableEntitiesManager", false, "Entity %s doesn't have the parent entity %s present in the spawnable", + clone->GetName().c_str(), parentId.ToString().data()); + } + } + // \TEMP } + // Let other systems know about newly spawned entities for any pre-processing before adding to the scene/game context. + if (request.m_preInsertionCallback) + { + request.m_preInsertionCallback(*request.m_ticket, SpawnableEntityContainerView( + ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end())); + } + + // 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) + { + GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, entity); + }); + + // Let other systems know about newly spawned entities for any post-processing after adding to the scene/game context. if (request.m_completionCallback) { request.m_completionCallback(*request.m_ticket, SpawnableConstEntityContainerView( - ticket.m_spawnedEntities.begin() + spawnedEntitiesCount, ticket.m_spawnedEntities.end())); + ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end())); } m_onSpawnedEvent.Signal(ticket.m_spawnable); @@ -249,24 +311,56 @@ namespace AzFramework Ticket& ticket = GetTicketPayload(*request.m_ticket); if (ticket.m_spawnable.IsReady() && request.m_ticketId == ticket.m_currentTicketId) { - size_t spawnedEntitiesCount = ticket.m_spawnedEntities.size(); + AZStd::vector& spawnedEntities = ticket.m_spawnedEntities; + AZStd::vector& spawnedEntityIndices = ticket.m_spawnedEntityIndices; - 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); + // Keep track 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(); + + spawnedEntities.reserve(spawnedEntities.size() + entitiesToSpawnSize); + spawnedEntityIndices.reserve(spawnedEntityIndices.size() + entitiesToSpawnSize); for (size_t index : request.m_entityIndices) { - ticket.m_spawnedEntities.push_back(SpawnSingleEntity(*entities[index], serializeContext)); - ticket.m_spawnedEntityIndices.push_back(index); + if (index < entitiesToSpawn.size()) + { + const AZ::Entity& entityTemplate = *entitiesToSpawn[index]; + + AZ::Entity* clone = serializeContext.CloneObject(&entityTemplate); + 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; + // Let other systems know about newly spawned entities for any pre-processing before adding to the scene/game context. + if (request.m_preInsertionCallback) + { + request.m_preInsertionCallback( + *request.m_ticket, + SpawnableEntityContainerView( + ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end())); + } + + // 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) + { + GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, entity); + }); + if (request.m_completionCallback) { request.m_completionCallback(*request.m_ticket, SpawnableConstEntityContainerView( - ticket.m_spawnedEntities.begin() + spawnedEntitiesCount, ticket.m_spawnedEntities.end())); + ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end())); } m_onSpawnedEvent.Signal(ticket.m_spawnable); diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h index c70b9ccaa6..54f48055fd 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h @@ -47,8 +47,8 @@ namespace AzFramework // The following functions are thread safe // - void SpawnAllEntities(EntitySpawnTicket& ticket, EntitySpawnCallback completionCallback = {}) override; - void SpawnEntities(EntitySpawnTicket& ticket, AZStd::vector entityIndices, + void SpawnAllEntities(EntitySpawnTicket& ticket, EntityPreInsertionCallback preInsertionCallback = {}, EntitySpawnCallback completionCallback = {}) override; + void SpawnEntities(EntitySpawnTicket& ticket, AZStd::vector entityIndices, EntityPreInsertionCallback preInsertionCallback = {}, EntitySpawnCallback completionCallback = {}) override; void DespawnAllEntities(EntitySpawnTicket& ticket, EntityDespawnCallback completionCallback = {}) override; @@ -90,6 +90,7 @@ namespace AzFramework struct SpawnAllEntitiesCommand { EntitySpawnCallback m_completionCallback; + EntityPreInsertionCallback m_preInsertionCallback; EntitySpawnTicket* m_ticket; uint32_t m_ticketId; }; @@ -97,6 +98,7 @@ namespace AzFramework { AZStd::vector m_entityIndices; EntitySpawnCallback m_completionCallback; + EntityPreInsertionCallback m_preInsertionCallback; EntitySpawnTicket* m_ticket; uint32_t m_ticketId; }; diff --git a/Gems/Multiplayer/Code/CMakeLists.txt b/Gems/Multiplayer/Code/CMakeLists.txt index 4eeee15c47..7a4eaeb014 100644 --- a/Gems/Multiplayer/Code/CMakeLists.txt +++ b/Gems/Multiplayer/Code/CMakeLists.txt @@ -59,6 +59,26 @@ ly_add_target( ) if (PAL_TRAIT_BUILD_HOST_TOOLS) + ly_add_target( + NAME Multiplayer.Tools.Static STATIC + NAMESPACE Gem + FILES_CMAKE + multiplayer_tools_files.cmake + COMPILE_DEFINITIONS + PUBLIC + MULTIPLAYER_TOOLS + INCLUDE_DIRECTORIES + PRIVATE + . + Source + ${pal_source_dir} + PUBLIC + Include + BUILD_DEPENDENCIES + PUBLIC + AZ::AzToolsFramework + Gem::Multiplayer.Static + ) ly_add_target( NAME Multiplayer.Tools MODULE @@ -74,8 +94,7 @@ if (PAL_TRAIT_BUILD_HOST_TOOLS) Include BUILD_DEPENDENCIES PRIVATE - AZ::AzToolsFramework - Gem::Multiplayer.Static + Gem::Multiplayer.Tools.Static ) ly_add_target( @@ -145,6 +164,30 @@ if (PAL_TRAIT_BUILD_TESTS_SUPPORTED) ly_add_googletest( NAME Gem::Multiplayer.Tests ) + + if (PAL_TRAIT_BUILD_HOST_TOOLS) + ly_add_target( + NAME Multiplayer.Tools.Tests ${PAL_TRAIT_TEST_TARGET_TYPE} + NAMESPACE Gem + FILES_CMAKE + multiplayer_tools_tests_files.cmake + INCLUDE_DIRECTORIES + PRIVATE + Tests + Source + . + BUILD_DEPENDENCIES + PRIVATE + AZ::AzTest + AZ::AzTestShared + AZ::AzToolsFrameworkTestCommon + Gem::Multiplayer.Tools.Static + ) + ly_add_googletest( + NAME Gem::Multiplayer.Tools.Tests + ) + endif() + endif() ly_add_target( diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/INetworkSpawnableLibrary.h b/Gems/Multiplayer/Code/Include/Multiplayer/INetworkSpawnableLibrary.h new file mode 100644 index 0000000000..422f7f4be3 --- /dev/null +++ b/Gems/Multiplayer/Code/Include/Multiplayer/INetworkSpawnableLibrary.h @@ -0,0 +1,34 @@ +/* +* 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 +#include +#include + +namespace Multiplayer +{ + //! @class INetworkSpawnableLibrary + //! @brief The interface for managing network spawnables. + class INetworkSpawnableLibrary + { + public: + AZ_RTTI(INetworkSpawnableLibrary, "{A3CF809C-6C1D-4B43-B2C4-3901B5DE1ABE}"); + + virtual ~INetworkSpawnableLibrary() = default; + virtual void BuildSpawnablesList() = 0; + virtual void ProcessSpawnableAsset(const AZStd::string& relativePath, AZ::Data::AssetId id) = 0; + virtual AZ::Name GetSpawnableNameFromAssetId(AZ::Data::AssetId assetId) = 0; + virtual AZ::Data::AssetId GetAssetIdByName(AZ::Name name) = 0; + }; +} diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/INetworkEntityManager.h b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/INetworkEntityManager.h index 72dbe201e5..4fc063333f 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/INetworkEntityManager.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/INetworkEntityManager.h @@ -78,6 +78,12 @@ namespace Multiplayer const AZ::Transform& transform ) = 0; + //! Configures new networked entity + //! @param netEntity the entity to setup + //! @param prefabEntryId the name of the spawnable the entity originated from + //! @param netEntityRole the net role the entity should be setup for + virtual void SetupNetEntity(AZ::Entity* netEntity, PrefabEntityId prefabEntityId, NetEntityRole netEntityRole) = 0; + //! Returns an ConstEntityPtr for the provided entityId. //! @param netEntityId the netEntityId to get an ConstEntityPtr for //! @return the requested ConstEntityPtr diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp index e47d142be8..ba8158ca18 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp @@ -555,10 +555,10 @@ namespace Multiplayer { replicatorEntity = entityList[0]; } - - AZ_Assert(replicatorEntity != nullptr, "Failed to create entity from prefab %s", prefabEntityId.m_prefabName.GetCStr()); - if (replicatorEntity == nullptr) + else { + AZ_Assert(false, "There should be exactly one created entity out of prefab %s, index %d. Got: %d", + prefabEntityId.m_prefabName.GetCStr(), prefabEntityId.m_entityOffset, entityList.size()); return false; } } diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.cpp index 6f3fdf2b23..dda3f18ad4 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.cpp @@ -34,18 +34,15 @@ namespace Multiplayer : m_networkEntityAuthorityTracker(*this) , m_removeEntitiesEvent([this] { RemoveEntities(); }, AZ::Name("NetworkEntityManager remove entities event")) , m_updateEntityDomainEvent([this] { UpdateEntityDomain(); }, AZ::Name("NetworkEntityManager update entity domain event")) - , m_onSpawnedHandler([this](AZ::Data::Asset spawnable) { this->OnSpawned(spawnable); }) - , m_onDespawnedHandler([this](AZ::Data::Asset spawnable) { this->OnDespawned(spawnable); }) { + AZ::Interface::Register(this); AzFramework::RootSpawnableNotificationBus::Handler::BusConnect(); - - AzFramework::SpawnableEntitiesInterface::Get()->AddOnSpawnedHandler(m_onSpawnedHandler); - AzFramework::SpawnableEntitiesInterface::Get()->AddOnDespawnedHandler(m_onDespawnedHandler); } NetworkEntityManager::~NetworkEntityManager() { AzFramework::RootSpawnableNotificationBus::Handler::BusDisconnect(); + AZ::Interface::Unregister(this); } void NetworkEntityManager::Initialize(HostId hostId, AZStd::unique_ptr entityDomain) @@ -331,17 +328,41 @@ namespace Multiplayer const AzFramework::Spawnable::EntityList& entities = spawnable.GetEntities(); size_t entitiesSize = entities.size(); + using EntityIdMap = AZStd::unordered_map; + EntityIdMap originalToCloneIdMap; + for (size_t i = 0; i < entitiesSize; ++i) { - AZ::Entity* clone = serializeContext->CloneObject(entities[i].get()); + AZ::Entity* originalEntity = entities[i].get(); + AZ::Entity* clone = serializeContext->CloneObject(originalEntity); AZ_Assert(clone != nullptr, "Failed to clone spawnable entity."); + clone->SetId(AZ::Entity::MakeId()); + originalToCloneIdMap[originalEntity->GetId()] = clone->GetId(); + NetBindComponent* netBindComponent = clone->FindComponent(); if (netBindComponent != nullptr) { + // Update TransformComponent parent Id. It is guaranteed for the entities array to be sorted from parent->child here. + auto* transformComponent = clone->FindComponent(); + AZ::EntityId parentId = transformComponent->GetParentId(); + if (parentId.IsValid()) + { + auto it = originalToCloneIdMap.find(parentId); + if (it != originalToCloneIdMap.end()) + { + transformComponent->SetParentRelative(it->second); + } + else + { + AZ_Warning("NetworkEntityManager", false, "Entity %s doesn't have the parent entity %s present in network.spawnable", + clone->GetName().c_str(), parentId.ToString().data()); + } + } + PrefabEntityId prefabEntityId; - prefabEntityId.m_prefabName = m_networkPrefabLibrary.GetPrefabNameFromAssetId(spawnable.GetId()); + prefabEntityId.m_prefabName = m_networkPrefabLibrary.GetSpawnableNameFromAssetId(spawnable.GetId()); prefabEntityId.m_entityOffset = aznumeric_cast(i); const NetEntityId netEntityId = NextId(); @@ -469,57 +490,19 @@ namespace Multiplayer } } - void NetworkEntityManager::OnSpawned(AZ::Data::Asset spawnable) + void NetworkEntityManager::SetupNetEntity(AZ::Entity* netEntity, PrefabEntityId prefabEntityId, NetEntityRole netEntityRole) { - AzFramework::Spawnable* spawnableData = spawnable.GetAs(); - const auto& entityList = spawnableData->GetEntities(); - if (entityList.size() == 0) + auto* netBindComponent = netEntity->FindComponent(); + + if (netBindComponent) { - AZ_Error("NetworkEntityManager", false, "OnSpawned: Spawnable %s doesn't have any entities.", - spawnable.GetHint().c_str()); - return; + const NetEntityId netEntityId = NextId(); + netBindComponent->PreInit(netEntity, prefabEntityId, netEntityId, netEntityRole); } - - const auto& rootEntity = entityList[0]; - auto* spawnableHolder = rootEntity->FindComponent(); - if (!spawnableHolder) + else { - // Root entity doesn't have NetworkSpawnableHolderComponent. It means there's no corresponding network spawnable. - return; + AZ_Error("NetworkEntityManager", false, "SetupNetEntity called for an entity with no NetBindComponent. Entity: %s", + netEntity->GetName().c_str()); } - - AZ::Data::Asset netSpawnableAsset = spawnableHolder->GetNetworkSpawnableAsset(); - AzFramework::Spawnable* netSpawnable = netSpawnableAsset.GetAs(); - if (!netSpawnable) - { - // TODO: Temp sync load until JsonSerialization of loadBehavior is fixed. - netSpawnableAsset = AZ::Data::AssetManager::Instance().GetAsset( - netSpawnableAsset.GetId(), AZ::Data::AssetLoadBehavior::PreLoad); - AZ::Data::AssetManager::Instance().BlockUntilLoadComplete(netSpawnableAsset); - - netSpawnable = netSpawnableAsset.GetAs(); - } - - if (!netSpawnable) - { - AZ_Error("NetworkEntityManager", false, "OnRootSpawnableAssigned: Net spawnable doesn't have any data."); - return; - } - - auto* multiplayer = GetMultiplayer(); - - const auto agentType = multiplayer->GetAgentType(); - const bool spawnImmediately = - (agentType == MultiplayerAgentType::ClientServer || agentType == MultiplayerAgentType::DedicatedServer); - - if (spawnImmediately) - { - CreateEntitiesImmediate(*netSpawnable, NetEntityRole::Authority); - } - } - - void NetworkEntityManager::OnDespawned([[maybe_unused]]AZ::Data::Asset spawnable) - { - // TODO: Remove entities instantiated from the spawnable } } diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.h b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.h index 3291f2c38d..dd9ac99a06 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.h +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.h @@ -62,6 +62,8 @@ namespace Multiplayer const AZ::Transform& transform ) override; + void SetupNetEntity(AZ::Entity* netEntity, PrefabEntityId prefabEntityId, NetEntityRole netEntityRole) override; + uint32_t GetEntityCount() const override; NetworkEntityHandle AddEntityToEntityMap(NetEntityId netEntityId, AZ::Entity* entity) override; void MarkForRemoval(const ConstNetworkEntityHandle& entityHandle) override; @@ -93,9 +95,6 @@ namespace Multiplayer void RemoveEntities(); NetEntityId NextId(); - void OnSpawned(AZ::Data::Asset spawnable); - void OnDespawned(AZ::Data::Asset spawnable); - NetworkEntityTracker m_networkEntityTracker; NetworkEntityAuthorityTracker m_networkEntityAuthorityTracker; MultiplayerComponentRegistry m_multiplayerComponentRegistry; @@ -123,8 +122,5 @@ namespace Multiplayer DeferredRpcMessages m_localDeferredRpcMessages; NetworkSpawnableLibrary m_networkPrefabLibrary; - - AZ::Event>::Handler m_onSpawnedHandler; - AZ::Event>::Handler m_onDespawnedHandler; }; } diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkSpawnableLibrary.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkSpawnableLibrary.cpp index ebf5d2609b..935744f807 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkSpawnableLibrary.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkSpawnableLibrary.cpp @@ -14,20 +14,23 @@ #include #include #include +#include namespace Multiplayer { NetworkSpawnableLibrary::NetworkSpawnableLibrary() { + AZ::Interface::Register(this); AzFramework::AssetCatalogEventBus::Handler::BusConnect(); } NetworkSpawnableLibrary::~NetworkSpawnableLibrary() { AzFramework::AssetCatalogEventBus::Handler::BusDisconnect(); + AZ::Interface::Unregister(this); } - void NetworkSpawnableLibrary::BuildPrefabsList() + void NetworkSpawnableLibrary::BuildSpawnablesList() { auto enumerateCallback = [this](const AZ::Data::AssetId id, const AZ::Data::AssetInfo& info) { @@ -50,10 +53,10 @@ namespace Multiplayer void NetworkSpawnableLibrary::OnCatalogLoaded([[maybe_unused]] const char* catalogFile) { - BuildPrefabsList(); + BuildSpawnablesList(); } - AZ::Name NetworkSpawnableLibrary::GetPrefabNameFromAssetId(AZ::Data::AssetId assetId) + AZ::Name NetworkSpawnableLibrary::GetSpawnableNameFromAssetId(AZ::Data::AssetId assetId) { if (assetId.IsValid()) { diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkSpawnableLibrary.h b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkSpawnableLibrary.h index a2c3d4ae56..6ebe2a7418 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkSpawnableLibrary.h +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkSpawnableLibrary.h @@ -12,30 +12,31 @@ #pragma once -#include -#include +#include #include -#include namespace Multiplayer { /// Implementation of the network prefab library interface. class NetworkSpawnableLibrary final - : private AzFramework::AssetCatalogEventBus::Handler + : public INetworkSpawnableLibrary + , private AzFramework::AssetCatalogEventBus::Handler { public: + AZ_RTTI(NetworkSpawnableLibrary, "{65E15F33-E893-49C2-A8E2-B6A8A6EF31E0}", INetworkSpawnableLibrary); + NetworkSpawnableLibrary(); ~NetworkSpawnableLibrary(); - void BuildPrefabsList(); - void ProcessSpawnableAsset(const AZStd::string& relativePath, AZ::Data::AssetId id); + /// INetworkSpawnableLibrary overrides. + void BuildSpawnablesList() override; + void ProcessSpawnableAsset(const AZStd::string& relativePath, AZ::Data::AssetId id) override; + AZ::Name GetSpawnableNameFromAssetId(AZ::Data::AssetId assetId) override; + AZ::Data::AssetId GetAssetIdByName(AZ::Name name) override; /// AssetCatalogEventBus overrides. void OnCatalogLoaded(const char* catalogFile) override; - AZ::Name GetPrefabNameFromAssetId(AZ::Data::AssetId assetId); - AZ::Data::AssetId GetAssetIdByName(AZ::Name name); - private: AZStd::unordered_map m_spawnables; AZStd::unordered_map m_spawnablesReverseLookup; diff --git a/Gems/Multiplayer/Code/Source/Pipeline/NetBindMarkerComponent.cpp b/Gems/Multiplayer/Code/Source/Pipeline/NetBindMarkerComponent.cpp index 84983b700e..1696e851a5 100644 --- a/Gems/Multiplayer/Code/Source/Pipeline/NetBindMarkerComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Pipeline/NetBindMarkerComponent.cpp @@ -11,7 +11,12 @@ */ #include +#include #include +#include +#include +#include +#include namespace Multiplayer { @@ -21,15 +26,91 @@ namespace Multiplayer if (serializeContext) { serializeContext->Class() - ->Version(1); + ->Version(1) + ->Field("NetEntityIndex", &NetBindMarkerComponent::m_netEntityIndex) + ->Field("NetSpawnableAsset", &NetBindMarkerComponent::m_networkSpawnableAsset); } } + AzFramework::Spawnable* GetSpawnableFromAsset(AZ::Data::Asset& asset) + { + AzFramework::Spawnable* spawnable = asset.GetAs(); + if (!spawnable) + { + asset = + AZ::Data::AssetManager::Instance().GetAsset(asset.GetId(), AZ::Data::AssetLoadBehavior::PreLoad); + AZ::Data::AssetManager::Instance().BlockUntilLoadComplete(asset); + + spawnable = asset.GetAs(); + } + + return spawnable; + } + + void NetBindMarkerComponent::Activate() { + const auto agentType = AZ::Interface::Get()->GetAgentType(); + const bool spawnImmediately = + (agentType == MultiplayerAgentType::ClientServer || agentType == MultiplayerAgentType::DedicatedServer); + + if (spawnImmediately && m_networkSpawnableAsset.GetId().IsValid()) + { + AZ::Transform worldTm = GetEntity()->FindComponent()->GetWorldTM(); + auto preInsertionCallback = + [worldTm = AZStd::move(worldTm), netEntityIndex = m_netEntityIndex, spawnableAssetId = m_networkSpawnableAsset.GetId()] + (AzFramework::EntitySpawnTicket&, AzFramework::SpawnableEntityContainerView entities) + { + if (entities.size() == 1) + { + AZ::Entity* netEntity = *entities.begin(); + + auto* transformComponent = netEntity->FindComponent(); + transformComponent->SetWorldTM(worldTm); + + AZ::Name spawnableName = AZ::Interface::Get()->GetSpawnableNameFromAssetId(spawnableAssetId); + PrefabEntityId prefabEntityId; + prefabEntityId.m_prefabName = spawnableName; + prefabEntityId.m_entityOffset = netEntityIndex; + AZ::Interface::Get()->SetupNetEntity(netEntity, prefabEntityId, NetEntityRole::Authority); + } + else + { + AZ_Error("NetBindMarkerComponent", false, "Requested to spawn 1 entity, but received %d", entities.size()); + } + }; + + m_netSpawnTicket = AzFramework::EntitySpawnTicket(m_networkSpawnableAsset); + AzFramework::SpawnableEntitiesInterface::Get()->SpawnEntities(m_netSpawnTicket, {m_netEntityIndex}, preInsertionCallback); + } } void NetBindMarkerComponent::Deactivate() { + if(m_netSpawnTicket.IsValid()) + { + AzFramework::SpawnableEntitiesInterface::Get()->DespawnAllEntities(m_netSpawnTicket); + } } + + size_t NetBindMarkerComponent::GetNetEntityIndex() const + { + return m_netEntityIndex; + } + + void NetBindMarkerComponent::SetNetEntityIndex(size_t netEntityIndex) + { + m_netEntityIndex = netEntityIndex; + } + + void NetBindMarkerComponent::SetNetworkSpawnableAsset(AZ::Data::Asset networkSpawnableAsset) + { + m_networkSpawnableAsset = networkSpawnableAsset; + } + + AZ::Data::Asset NetBindMarkerComponent::GetNetworkSpawnableAsset() const + { + return m_networkSpawnableAsset; + } + } diff --git a/Gems/Multiplayer/Code/Source/Pipeline/NetBindMarkerComponent.h b/Gems/Multiplayer/Code/Source/Pipeline/NetBindMarkerComponent.h index ebafead73c..5ab42ab3aa 100644 --- a/Gems/Multiplayer/Code/Source/Pipeline/NetBindMarkerComponent.h +++ b/Gems/Multiplayer/Code/Source/Pipeline/NetBindMarkerComponent.h @@ -13,6 +13,9 @@ #pragma once #include +#include +#include +#include namespace Multiplayer { @@ -34,6 +37,15 @@ namespace Multiplayer void Deactivate() override; //! @} + size_t GetNetEntityIndex() const; + void SetNetEntityIndex(size_t val); + + void SetNetworkSpawnableAsset(AZ::Data::Asset networkSpawnableAsset); + AZ::Data::Asset GetNetworkSpawnableAsset() const; + private: + AZ::Data::Asset m_networkSpawnableAsset{AZ::Data::AssetLoadBehavior::PreLoad}; + size_t m_netEntityIndex = 0; + AzFramework::EntitySpawnTicket m_netSpawnTicket; }; } // namespace Multiplayer diff --git a/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp b/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp index 4661df7564..93136ab261 100644 --- a/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp +++ b/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp @@ -59,6 +59,7 @@ namespace Multiplayer return result; } + void NetworkPrefabProcessor::ProcessPrefab(PrefabProcessorContext& context, AZStd::string_view prefabName, PrefabDom& prefab) { using namespace AzToolsFramework::Prefab; @@ -113,29 +114,35 @@ namespace Multiplayer AZStd::unique_ptr networkInstance(aznew Instance()); - for (auto entityId : networkedEntityIds) - { - AZ::Entity* netEntity = sourceInstance->DetachEntity(entityId).release(); + AZ::Data::Asset networkSpawnableAsset; + networkSpawnableAsset.Create(networkSpawnable->GetId()); + networkSpawnableAsset.SetAutoLoadBehavior(AZ::Data::AssetLoadBehavior::PreLoad); + for (size_t entityIndex = 0; entityIndex < networkedEntityIds.size(); ++entityIndex) + { + AZ::EntityId entityId = networkedEntityIds[entityIndex]; + + AZ::Entity* netEntity = sourceInstance->DetachEntity(entityId).release(); + // Net entity will need a new ID to avoid IDs collision + netEntity->SetId(AZ::Entity::MakeId()); networkInstance->AddEntity(*netEntity); - AZ::Entity* breadcrumbEntity = aznew AZ::Entity(netEntity->GetName()); + // Use the old ID for the breadcrumb entity to keep parent-child relationship in the original spawnable + AZ::Entity* breadcrumbEntity = aznew AZ::Entity(entityId, netEntity->GetName()); breadcrumbEntity->SetRuntimeActiveByDefault(netEntity->IsRuntimeActiveByDefault()); - breadcrumbEntity->CreateComponent(); + + NetBindMarkerComponent* netBindMarkerComponent = breadcrumbEntity->CreateComponent(); + // Each spawnable has a root meta-data entity at position 0, so starting net indices from 1 + netBindMarkerComponent->SetNetEntityIndex(entityIndex + 1); + netBindMarkerComponent->SetNetworkSpawnableAsset(networkSpawnableAsset); AzFramework::TransformComponent* transformComponent = netEntity->FindComponent(); breadcrumbEntity->CreateComponent(*transformComponent); - - // TODO: Configure NetBindMarkerComponent to refer to the net entity + sourceInstance->AddEntity(*breadcrumbEntity); } // Add net spawnable asset holder { - AZ::Data::AssetId assetId = networkSpawnable->GetId(); - AZ::Data::Asset networkSpawnableAsset; - networkSpawnableAsset.Create(assetId); - networkSpawnableAsset.SetAutoLoadBehavior(AZ::Data::AssetLoadBehavior::PreLoad); - EntityOptionalReference containerEntityRef = sourceInstance->GetContainerEntity(); if (containerEntityRef.has_value()) { @@ -175,6 +182,9 @@ namespace Multiplayer (*it)->InvalidateDependencies(); (*it)->EvaluateDependencies(); } + + SpawnableUtils::SortEntitiesByTransformHierarchy(*networkSpawnable); + context.GetProcessedObjects().push_back(AZStd::move(object)); } else diff --git a/Gems/Multiplayer/Code/Tests/MainTools.cpp b/Gems/Multiplayer/Code/Tests/MainTools.cpp new file mode 100644 index 0000000000..ccb4d568f8 --- /dev/null +++ b/Gems/Multiplayer/Code/Tests/MainTools.cpp @@ -0,0 +1,55 @@ +/* +* 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 +#include +#include +#include +#include + +namespace Multiplayer +{ + class MultiplayerToolsTestEnvironment : public AZ::Test::GemTestEnvironment + { + AZ::ComponentApplication* CreateApplicationInstance() override + { + return aznew UnitTest::ToolsTestApplication("MultiplayerToolsTest"); + } + + void AddGemsAndComponents() override + { + AZStd::vector descriptors({ + NetBindComponent::CreateDescriptor(), + NetBindMarkerComponent::CreateDescriptor(), + NetworkSpawnableHolderComponent::CreateDescriptor() + }); + + AddComponentDescriptors(descriptors); + } + }; +} // namespace UnitTest + +// Required to support running integration tests with Qt +AZTEST_EXPORT int AZ_UNIT_TEST_HOOK_NAME(int argc, char** argv) +{ + ::testing::InitGoogleMock(&argc, argv); + AzQtComponents::PrepareQtPaths(); + QApplication app(argc, argv); + AZ::Test::printUnusedParametersWarning(argc, argv); + AZ::Test::addTestEnvironments({new Multiplayer::MultiplayerToolsTestEnvironment}); + int result = RUN_ALL_TESTS(); + return result; +} diff --git a/Gems/Multiplayer/Code/Tests/PrefabProcessingTests.cpp b/Gems/Multiplayer/Code/Tests/PrefabProcessingTests.cpp new file mode 100644 index 0000000000..c441d2ba5f --- /dev/null +++ b/Gems/Multiplayer/Code/Tests/PrefabProcessingTests.cpp @@ -0,0 +1,114 @@ +/* +* 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 +#include +#include +#include + +namespace UnitTest +{ + class PrefabProcessingTestFixture : public ::testing::Test + { + public: + static void ConvertEntitiesToPrefab(const AZStd::vector& entities, AzToolsFramework::Prefab::PrefabDom& prefabDom) + { + auto* prefabSystem = AZ::Interface::Get(); + AZStd::unique_ptr sourceInstance(prefabSystem->CreatePrefab(entities, {}, "test/path")); + ASSERT_TRUE(sourceInstance); + + auto& prefabTemplateDom = prefabSystem->FindTemplateDom(sourceInstance->GetTemplateId()); + prefabDom.CopyFrom(prefabTemplateDom, prefabDom.GetAllocator()); + } + + static AZ::Entity* CreateSourceEntity(const char* name, bool networked, const AZ::Transform& tm, AZ::Entity* parent = nullptr) + { + AZ::Entity* entity = aznew AZ::Entity(name); + auto* transformComponent = entity->CreateComponent(); + + if (parent) + { + transformComponent->SetParent(parent->GetId()); + transformComponent->SetLocalTM(tm); + } + else + { + transformComponent->SetWorldTM(tm); + } + + if(networked) + { + entity->CreateComponent(); + } + + return entity; + } + }; + + TEST_F(PrefabProcessingTestFixture, NetworkPrefabProcessor_ProcessPrefabTwoEntities_NetEntityGoesToNetSpawnable) + { + using AzToolsFramework::Prefab::PrefabConversionUtils::PrefabProcessorContext; + + AZStd::vector entities; + + // Create test entities: 1 networked and 1 static + const AZStd::string staticEntityName = "static_floor"; + entities.emplace_back(CreateSourceEntity(staticEntityName.c_str(), false, AZ::Transform::CreateIdentity())); + + const AZStd::string netEntityName = "networked_entity"; + entities.emplace_back(CreateSourceEntity(netEntityName.c_str(), true, AZ::Transform::CreateIdentity())); + + // Convert the entities into prefab. Note: This will transfer the ownership of AZ::Entity* into Prefab + AzToolsFramework::Prefab::PrefabDom prefabDom; + ConvertEntitiesToPrefab(entities, prefabDom); + + // Add the prefab into the Prefab Processor Context + const AZStd::string prefabName = "testPrefab"; + PrefabProcessorContext prefabProcessorContext{AZ::Uuid::CreateRandom()}; + prefabProcessorContext.AddPrefab(prefabName, AZStd::move(prefabDom)); + + // Request NetworkPrefabProcessor to process the prefab + Multiplayer::NetworkPrefabProcessor processor; + processor.Process(prefabProcessorContext); + + // Validate results + EXPECT_TRUE(prefabProcessorContext.HasCompletedSuccessfully()); + + // Should be 1 networked spawnable + const auto& processedObjects = prefabProcessorContext.GetProcessedObjects(); + EXPECT_EQ(processedObjects.size(), 1); + + // Verify the name and the type of the spawnable asset + const AZ::Data::AssetData& spawnableAsset = processedObjects[0].GetAsset(); + EXPECT_EQ(prefabName + ".network.spawnable", processedObjects[0].GetId()); + EXPECT_EQ(spawnableAsset.GetType(), azrtti_typeid()); + + // Verify we have only the networked entity in the network spawnable and not the static one + const AzFramework::Spawnable* netSpawnable = azrtti_cast(&spawnableAsset); + const AzFramework::Spawnable::EntityList& entityList = netSpawnable->GetEntities(); + auto countEntityCallback = [](const auto& name) + { + return [name](const auto& entity) + { + return entity->GetName() == name; + }; + }; + + EXPECT_EQ(0, AZStd::count_if(entityList.begin(), entityList.end(), countEntityCallback(staticEntityName))); + EXPECT_EQ(1, AZStd::count_if(entityList.begin(), entityList.end(), countEntityCallback(netEntityName))); + } + +} // namespace UnitTest diff --git a/Gems/Multiplayer/Code/multiplayer_files.cmake b/Gems/Multiplayer/Code/multiplayer_files.cmake index 8dd7c07689..addd4ea810 100644 --- a/Gems/Multiplayer/Code/multiplayer_files.cmake +++ b/Gems/Multiplayer/Code/multiplayer_files.cmake @@ -22,6 +22,7 @@ set(FILES Include/Multiplayer/ConnectionData/IConnectionData.h Include/Multiplayer/EntityDomains/IEntityDomain.h Include/Multiplayer/NetworkEntity/INetworkEntityManager.h + Include/Multiplayer/INetworkSpawnableLibrary.h Include/Multiplayer/NetworkEntity/NetworkEntityRpcMessage.h Include/Multiplayer/NetworkEntity/NetworkEntityUpdateMessage.h Include/Multiplayer/NetworkEntity/NetworkEntityHandle.h diff --git a/Gems/Multiplayer/Code/multiplayer_tools_tests_files.cmake b/Gems/Multiplayer/Code/multiplayer_tools_tests_files.cmake new file mode 100644 index 0000000000..c308b3de52 --- /dev/null +++ b/Gems/Multiplayer/Code/multiplayer_tools_tests_files.cmake @@ -0,0 +1,15 @@ +# +# 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. +# + +set(FILES + Tests/MainTools.cpp + Tests/PrefabProcessingTests.cpp +)