diff --git a/Gems/Multiplayer/Code/CMakeLists.txt b/Gems/Multiplayer/Code/CMakeLists.txt index 909590a25d..b971e6a2ce 100644 --- a/Gems/Multiplayer/Code/CMakeLists.txt +++ b/Gems/Multiplayer/Code/CMakeLists.txt @@ -175,6 +175,11 @@ if (PAL_TRAIT_BUILD_TESTS_SUPPORTED) PRIVATE AZ::AzTest Gem::Multiplayer.Static + AUTOGEN_RULES + *.AutoComponent.xml,AutoComponent_Header.jinja,$path/$fileprefix.AutoComponent.h + *.AutoComponent.xml,AutoComponent_Source.jinja,$path/$fileprefix.AutoComponent.cpp + *.AutoComponent.xml,AutoComponentTypes_Header.jinja,$path/AutoComponentTypes.h + *.AutoComponent.xml,AutoComponentTypes_Source.jinja,$path/AutoComponentTypes.cpp ) ly_add_googletest( NAME Gem::Multiplayer.Tests diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/Components/LocalPredictionPlayerInputComponent.h b/Gems/Multiplayer/Code/Include/Multiplayer/Components/LocalPredictionPlayerInputComponent.h index bcbea6542f..25fe473135 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/Components/LocalPredictionPlayerInputComponent.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/Components/LocalPredictionPlayerInputComponent.h @@ -71,6 +71,8 @@ namespace Multiplayer void UpdateAutonomous(AZ::TimeMs deltaTimeMs); void UpdateBankedTime(AZ::TimeMs deltaTimeMs); + bool SerializeEntityCorrection(AzNetworking::ISerializer& serializer); + using StateHistoryItem = AZStd::unique_ptr; AZStd::map m_predictiveStateHistory; diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyRootComponent.h b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyRootComponent.h index 674dc6114d..f0ad3baf94 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyRootComponent.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyRootComponent.h @@ -29,6 +29,8 @@ namespace Multiplayer , public NetworkHierarchyRequestBus::Handler { friend class NetworkHierarchyChildComponent; + friend class NetworkHierarchyRootComponentController; + friend class ServerToClientReplicationWindow; public: AZ_MULTIPLAYER_COMPONENT(Multiplayer::NetworkHierarchyRootComponent, s_networkHierarchyRootComponentConcreteUuid, Multiplayer::NetworkHierarchyRootComponentBase); @@ -57,6 +59,8 @@ namespace Multiplayer void BindNetworkHierarchyLeaveEventHandler(NetworkHierarchyLeaveEvent::Handler& handler) override; //! @} + bool SerializeEntityCorrection(AzNetworking::ISerializer& serializer); + protected: void SetTopLevelHierarchyRootEntity(AZ::Entity* hierarchyRoot); @@ -99,4 +103,24 @@ namespace Multiplayer friend class HierarchyBenchmarkBase; }; + + + //! NetworkHierarchyRootComponentController + //! This is the network controller for NetworkHierarchyRootComponent. + //! Class provides the ability to process input for hierarchies. + class NetworkHierarchyRootComponentController final + : public NetworkHierarchyRootComponentControllerBase + { + public: + NetworkHierarchyRootComponentController(NetworkHierarchyRootComponent& parent); + + // NetworkHierarchyRootComponentControllerBase + void OnActivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + void OnDeactivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + + //! MultiplayerController interface + Multiplayer::MultiplayerController::InputPriorityOrder GetInputOrder() const override; + void CreateInput(Multiplayer::NetworkInput& input, float deltaTime) override; + void ProcessInput(Multiplayer::NetworkInput& input, float deltaTime) override; + }; } diff --git a/Gems/Multiplayer/Code/Source/AutoGen/LocalPredictionPlayerInputComponent.AutoComponent.xml b/Gems/Multiplayer/Code/Source/AutoGen/LocalPredictionPlayerInputComponent.AutoComponent.xml index 1a7496a77d..194c05577d 100644 --- a/Gems/Multiplayer/Code/Source/AutoGen/LocalPredictionPlayerInputComponent.AutoComponent.xml +++ b/Gems/Multiplayer/Code/Source/AutoGen/LocalPredictionPlayerInputComponent.AutoComponent.xml @@ -9,6 +9,7 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + diff --git a/Gems/Multiplayer/Code/Source/AutoGen/NetworkHierarchyRootComponent.AutoComponent.xml b/Gems/Multiplayer/Code/Source/AutoGen/NetworkHierarchyRootComponent.AutoComponent.xml index 0f33e1f642..fbe6ff2135 100644 --- a/Gems/Multiplayer/Code/Source/AutoGen/NetworkHierarchyRootComponent.AutoComponent.xml +++ b/Gems/Multiplayer/Code/Source/AutoGen/NetworkHierarchyRootComponent.AutoComponent.xml @@ -4,11 +4,15 @@ Name="NetworkHierarchyRootComponent" Namespace="Multiplayer" OverrideComponent="true" - OverrideController="false" + OverrideController="true" OverrideInclude="Multiplayer/Components/NetworkHierarchyRootComponent.h" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + + - + + + diff --git a/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp b/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp index 747f53d37b..4d0756d9a5 100644 --- a/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp @@ -14,6 +14,7 @@ #include #include #include +#include namespace Multiplayer { @@ -211,7 +212,7 @@ namespace Multiplayer m_lastCorrectionSentTimeMs = currentTimeMs; AzNetworking::HashSerializer hashSerializer; - GetNetBindComponent()->SerializeEntityCorrection(hashSerializer); + SerializeEntityCorrection(hashSerializer); const AZ::HashValue32 localAuthorityHash = hashSerializer.GetHash(); @@ -233,7 +234,7 @@ namespace Multiplayer // only deserialize if we have data (for client/server profile/debug mismatches) if (correction.GetSize() > 0) { - GetNetBindComponent()->SerializeEntityCorrection(serializer); + SerializeEntityCorrection(serializer); } correction.Resize(serializer.GetSize()); @@ -313,7 +314,7 @@ namespace Multiplayer // Apply the correction AzNetworking::TrackChangedSerializer serializer(correction.GetBuffer(), static_cast(correction.GetSize())); - GetNetBindComponent()->SerializeEntityCorrection(serializer); + SerializeEntityCorrection(serializer); GetNetBindComponent()->NotifyCorrection(); #ifndef AZ_RELEASE_BUILD @@ -325,7 +326,7 @@ namespace Multiplayer { // Read out state values AzNetworking::StringifySerializer serverValues; - GetNetBindComponent()->SerializeEntityCorrection(serverValues); + SerializeEntityCorrection(serverValues); PrintCorrectionDifferences(*iter->second, serverValues); } else @@ -452,7 +453,7 @@ namespace Multiplayer // Generate a hash based on the current client predicted states AzNetworking::HashSerializer hashSerializer; - GetNetBindComponent()->SerializeEntityCorrection(hashSerializer); + SerializeEntityCorrection(hashSerializer); // Save this input and discard move history outside our client rewind window m_inputHistory.PushBack(input); @@ -480,7 +481,7 @@ namespace Multiplayer { m_predictiveStateHistory.erase(m_predictiveStateHistory.begin()); } - GetNetBindComponent()->SerializeEntityCorrection(*inputHistory); + SerializeEntityCorrection(*inputHistory); m_predictiveStateHistory.emplace(m_clientInputId, AZStd::move(inputHistory)); } #endif @@ -493,6 +494,18 @@ namespace Multiplayer } } + bool LocalPredictionPlayerInputComponentController::SerializeEntityCorrection(AzNetworking::ISerializer& serializer) + { + bool result = GetNetBindComponent()->SerializeEntityCorrection(serializer); + + NetworkHierarchyRootComponent* hierarchyComponent = GetParent().GetNetworkHierarchyRootComponent(); + if (result && hierarchyComponent) + { + result = hierarchyComponent->SerializeEntityCorrection(serializer); + } + return result; + } + void LocalPredictionPlayerInputComponentController::UpdateBankedTime(AZ::TimeMs deltaTimeMs) { const double deltaTime = static_cast(deltaTimeMs) / 1000.0; diff --git a/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyRootComponent.cpp b/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyRootComponent.cpp index 4a5e9ce8d2..e45389b7d9 100644 --- a/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyRootComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyRootComponent.cpp @@ -326,4 +326,150 @@ namespace Multiplayer RebuildHierarchy(); } } + + NetworkHierarchyRootComponentController::NetworkHierarchyRootComponentController(NetworkHierarchyRootComponent& parent) + : NetworkHierarchyRootComponentControllerBase(parent) + { + + } + + void NetworkHierarchyRootComponentController::OnActivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + + } + + void NetworkHierarchyRootComponentController::OnDeactivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + + } + + Multiplayer::MultiplayerController::InputPriorityOrder NetworkHierarchyRootComponentController::GetInputOrder() const + { + return Multiplayer::MultiplayerController::InputPriorityOrder::SubEntities; + } + + void NetworkHierarchyRootComponentController::CreateInput(Multiplayer::NetworkInput& input, float deltaTime) + { + NetworkHierarchyRootComponent& component = GetParent(); + if(!component.IsHierarchicalRoot()) + { + return; + } + + INetworkEntityManager* networkEntityManager = AZ::Interface::Get(); + AZ_Assert(networkEntityManager, "NetworkEntityManager must be created."); + + const AZStd::vector& entities = component.m_hierarchicalEntities; + + auto* networkInput = input.FindComponentInput(); + networkInput->m_childInputs.clear(); + networkInput->m_childInputs.reserve(entities.size()); + + for (AZ::Entity* child : entities) + { + if(child == component.GetEntity()) + { + continue; // Avoid infinite recursion + } + + NetEntityId childNetEntitydId = networkEntityManager->GetNetEntityIdById(child->GetId()); + AZ_Assert(childNetEntitydId != InvalidNetEntityId, "Unable to find the hierarchy entity in Network Entity Manager"); + + ConstNetworkEntityHandle childEntityHandle = networkEntityManager->GetEntity(childNetEntitydId); + NetBindComponent* netComp = childEntityHandle.GetNetBindComponent(); + + AZ_Assert(netComp, "No NetBindComponent, this should be impossible"); + // Validate we still have a controller and we aren't in the middle of removing them + if (netComp->HasController()) + { + NetworkInputChild subInput; + subInput.Attach(childEntityHandle); + subInput.GetNetworkInput().SetClientInputId(input.GetClientInputId()); + + netComp->CreateInput(subInput.GetNetworkInput(), deltaTime); + + // make sure our input sub commands have the same time as the original + subInput.GetNetworkInput().SetClientInputId(input.GetClientInputId()); + networkInput->m_childInputs.emplace_back(subInput); + } + } + } + + void NetworkHierarchyRootComponentController::ProcessInput(Multiplayer::NetworkInput& input, float deltaTime) + { + if (auto* networkInput = input.FindComponentInput()) + { + INetworkEntityManager* networkEntityManager = AZ::Interface::Get(); + AZ_Assert(networkEntityManager, "NetworkEntityManager must be created."); + + // Build a set of Net IDs for the children + AZStd::unordered_set currentChildren; + NetworkHierarchyRootComponent& component = GetParent(); + for (AZ::Entity* child : component.m_hierarchicalEntities) + { + if (child == component.GetEntity()) // Skip the root entity + { + continue; + } + + NetEntityId childNetEntitydId = networkEntityManager->GetNetEntityIdById(child->GetId()); + AZ_Assert(childNetEntitydId != InvalidNetEntityId, "Unable to find the hierarchy entity in Network Entity Manager"); + currentChildren.insert(childNetEntitydId); + } + + // Process the input for the child entities + for (NetworkInputChild& subInput : networkInput->m_childInputs) + { + const ConstNetworkEntityHandle& inputOwnerHandle = subInput.GetOwner(); + NetEntityId inputOwnerNetEntitydId = inputOwnerHandle.GetNetEntityId(); + + if (currentChildren.count(inputOwnerNetEntitydId) == 0) + { + // Skip the input for entities which are not a part of this hierarchy + continue; + } + + ConstNetworkEntityHandle localEntityHandle = networkEntityManager->GetEntity(inputOwnerNetEntitydId); + if (localEntityHandle.Exists()) + { + auto* netComp = localEntityHandle.GetNetBindComponent(); + AZ_Assert(netComp, "No NetBindComponent, this should be impossible"); + // We do not rewind entity role changes, so make sure we are the correct role prior to processing + if (netComp->HasController()) + { + subInput.GetNetworkInput().SetClientInputId(input.GetClientInputId()); + netComp->ProcessInput(subInput.GetNetworkInput(), deltaTime); + } + } + } + } + } + + bool NetworkHierarchyRootComponent::SerializeEntityCorrection(AzNetworking::ISerializer& serializer) + { + bool result = true; + + INetworkEntityManager* networkEntityManager = AZ::Interface::Get(); + AZ_Assert(networkEntityManager, "NetworkEntityManager must be created."); + + for (AZ::Entity* child : m_hierarchicalEntities) + { + if (child == GetEntity()) + { + // Skip the root entity + continue; + } + + NetEntityId childNetEntitydId = networkEntityManager->GetNetEntityIdById(child->GetId()); + AZ_Assert(childNetEntitydId != InvalidNetEntityId, "Unable to find the hierarchy entity in Network Entity Manager"); + + ConstNetworkEntityHandle childEntityHandle = networkEntityManager->GetEntity(childNetEntitydId); + NetBindComponent* netBindComponent = childEntityHandle.GetNetBindComponent(); + AZ_Assert(netBindComponent, "No NetBindComponent, this should be impossible"); + + result = result && netBindComponent->SerializeEntityCorrection(serializer); + } + + return result; + } } diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp index e8710cdd6c..42a7fe43e5 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp @@ -685,12 +685,21 @@ namespace Multiplayer if (GetAgentType() == MultiplayerAgentType::ClientServer || GetAgentType() == MultiplayerAgentType::DedicatedServer) { - NetworkEntityHandle controlledEntity = SpawnDefaultPlayerPrefab(); - if (controlledEntity.Exists()) + INetworkEntityManager::EntityList entityList = SpawnDefaultPlayerPrefab(); + for (auto& netEntity : entityList) { - controlledEntity.GetNetBindComponent()->SetOwningConnectionId(connection->GetConnectionId()); + if (netEntity.Exists()) + { + netEntity.GetNetBindComponent()->SetOwningConnectionId(connection->GetConnectionId()); + } + netEntity.Activate(); + } + + NetworkEntityHandle controlledEntity; + if (entityList.size() > 0) + { + controlledEntity = entityList[0]; } - controlledEntity.Activate(); connection->SetUserData(new ServerToClientConnectionData(connection, *this, controlledEntity)); AZStd::unique_ptr window = AZStd::make_unique(controlledEntity, connection); @@ -800,12 +809,16 @@ namespace Multiplayer // Spawn the default player for this host since the host is also a player (not a dedicated server) if (m_agentType == MultiplayerAgentType::ClientServer) { - NetworkEntityHandle controlledEntity = SpawnDefaultPlayerPrefab(); - if (NetBindComponent* controlledEntityNetBindComponent = controlledEntity.GetNetBindComponent()) + INetworkEntityManager::EntityList entityList = SpawnDefaultPlayerPrefab(); + + for (NetworkEntityHandle controlledEntity : entityList) { - controlledEntityNetBindComponent->SetAllowAutonomy(true); + if (NetBindComponent* controlledEntityNetBindComponent = controlledEntity.GetNetBindComponent()) + { + controlledEntityNetBindComponent->SetAllowAutonomy(true); + } + controlledEntity.Activate(); } - controlledEntity.Activate(); } AZLOG_INFO("Multiplayer operating in %s mode", GetEnumString(m_agentType)); @@ -1053,17 +1066,12 @@ namespace Multiplayer } } - NetworkEntityHandle MultiplayerSystemComponent::SpawnDefaultPlayerPrefab() + INetworkEntityManager::EntityList MultiplayerSystemComponent::SpawnDefaultPlayerPrefab() { PrefabEntityId playerPrefabEntityId(AZ::Name(static_cast(sv_defaultPlayerSpawnAsset).c_str())); INetworkEntityManager::EntityList entityList = m_networkEntityManager.CreateEntitiesImmediate(playerPrefabEntityId, NetEntityRole::Authority, AZ::Transform::CreateIdentity(), Multiplayer::AutoActivate::DoNotActivate); - NetworkEntityHandle controlledEntity; - if (!entityList.empty()) - { - controlledEntity = entityList[0]; - } - return controlledEntity; + return entityList; } void host([[maybe_unused]] const AZ::ConsoleCommandContainer& arguments) diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h index 568d5f7801..9ab78e0f0b 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h @@ -146,7 +146,7 @@ namespace Multiplayer void TickVisibleNetworkEntities(float deltaTime, float serverRateSeconds); void OnConsoleCommandInvoked(AZStd::string_view command, const AZ::ConsoleCommandContainer& args, AZ::ConsoleFunctorFlags flags, AZ::ConsoleInvokedFrom invokedFrom); void ExecuteConsoleCommandList(AzNetworking::IConnection* connection, const AZStd::fixed_vector& commands); - NetworkEntityHandle SpawnDefaultPlayerPrefab(); + INetworkEntityManager::EntityList SpawnDefaultPlayerPrefab(); AZ_CONSOLEFUNC(MultiplayerSystemComponent, DumpStats, AZ::ConsoleFunctorFlags::Null, "Dumps stats for the current multiplayer session"); diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityHandle.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityHandle.cpp index ef338840f9..7794d9026b 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityHandle.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityHandle.cpp @@ -32,8 +32,8 @@ namespace Multiplayer if (entity) { - AZ_Assert(networkEntityTracker, "NetworkEntityTracker is not valid"); - m_netBindComponent = networkEntityTracker->GetNetBindComponent(entity); + AZ_Assert(m_networkEntityTracker, "NetworkEntityTracker is not valid"); + m_netBindComponent = m_networkEntityTracker->GetNetBindComponent(entity); if (m_netBindComponent != nullptr) { m_netEntityId = m_netBindComponent->GetNetEntityId(); diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.cpp index db7f7243cc..b72ea21719 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.cpp @@ -21,6 +21,8 @@ #include #include #include +#include +#include #include namespace Multiplayer @@ -272,15 +274,32 @@ namespace Multiplayer bool safeToExit = true; NetworkEntityHandle entityHandle = m_networkEntityTracker.Get(entityId); - // We also need special handling for the EntityHierarchyComponent as well, since related entities need to be migrated together - //auto* hierarchyController = FindController(nonConstExitingEntityPtr); - //if (hierarchyController) - //{ - // if (hierarchyController->GetParentRelatedEntity()) - // { - // safeToExit = false; - // } - //} + // We also need special handling for the NetworkHierarchy as well, since related entities need to be migrated together + NetworkHierarchyRootComponentController* hierarchyRootController = entityHandle.FindController(); + NetworkHierarchyChildComponentController* hierarchyChildController = entityHandle.FindController(); + + // Find the root entity + AZ::Entity* hierarchyRootEntity = nullptr; + if (hierarchyRootController) + { + hierarchyRootEntity = hierarchyRootController->GetParent().GetHierarchicalRoot(); + } + else if (hierarchyChildController) + { + hierarchyRootEntity = hierarchyChildController->GetParent().GetHierarchicalRoot(); + } + + if (hierarchyRootEntity) + { + NetEntityId rootNetId = GetNetEntityIdById(hierarchyRootEntity->GetId()); + ConstNetworkEntityHandle rootEntityHandle = GetEntity(rootNetId); + + // Check if the root entity is still tracked by this authority + if (rootEntityHandle.Exists() && rootEntityHandle.GetNetBindComponent()->HasController()) + { + safeToExit = false; + } + } // Validate that we aren't already planning to remove this entity if (safeToExit) diff --git a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputChild.h b/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputChild.h index 98778cce17..417d9d65f7 100644 --- a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputChild.h +++ b/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputChild.h @@ -12,10 +12,7 @@ namespace Multiplayer { - //! Max number of entities that can be children of our netbound player entity. - static constexpr uint32_t MaxEntityHierarchyChildren = 16; - - //! Used by the EntityHierarchyComponent. This component allows the gameplay programmer to specify inputs for dependent entities. + //! Used by the NetworkHierarchyRootComponent. This component allows the gameplay programmer to specify inputs for dependent entities. //! Since it is possible to for the Client/Server to disagree about the state of related entities, //! this network input encodes the entity that is associated with it. class NetworkInputChild @@ -37,4 +34,6 @@ namespace Multiplayer ConstNetworkEntityHandle m_owner; NetworkInput m_networkInput; }; + + using NetworkInputChildList = AZStd::vector; } diff --git a/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp b/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp index e0990aa785..27e59c0bf4 100644 --- a/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp +++ b/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp @@ -110,7 +110,7 @@ namespace Multiplayer auto serializer = [](AZStd::vector& output, const ProcessedObjectStore& object) -> bool { AZ::IO::ByteContainerStream stream(&output); auto& asset = object.GetAsset(); - return AZ::Utils::SaveObjectToStream(stream, AZ::DataStream::ST_JSON, &asset, asset.GetType()); + return AZ::Utils::SaveObjectToStream(stream, AZ::DataStream::ST_BINARY, &asset, asset.GetType()); }; auto&& [object, networkSpawnable] = diff --git a/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.cpp b/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.cpp index 9d9cc74d2b..856fd59f9d 100644 --- a/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.cpp +++ b/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -174,11 +175,11 @@ namespace Multiplayer // Note: Do not add any Client entities after this point, otherwise you stomp over the Autonomous mode m_replicationSet[m_controlledEntity] = { NetEntityRole::Autonomous, 1.0f }; // Always replicate autonomous entities - //auto hierarchyController = FindController(m_ControlledEntity); - //if (hierarchyController != nullptr) - //{ - // CollectControlledEntitiesRecursive(m_replicationSet, *hierarchyController); - //} + auto* hierarchyComponent = m_controlledEntity.FindComponent(); + if (hierarchyComponent != nullptr) + { + UpdateHierarchyReplicationSet(m_replicationSet, *hierarchyComponent); + } } AzNetworking::PacketId ServerToClientReplicationWindow::SendEntityUpdateMessages(NetworkEntityUpdateVector& entityUpdateVector) @@ -326,18 +327,20 @@ namespace Multiplayer } } - //void ServerToClientReplicationWindow::CollectControlledEntitiesRecursive(ReplicationSet& replicationSet, EntityHierarchyComponent::Authority& hierarchyController) - //{ - // auto controlledEnts = hierarchyController.GetChildrenRelatedEntities(); - // for (auto& controlledEnt : controlledEnts) - // { - // AZ_Assert(controlledEnt != nullptr, "We have lost a controlled entity unexpectedly"); - // replicationSet[controlledEnt.GetConstEntity()] = EntityReplicationData(EntityNetworkRoleT::e_Autonomous, EntityPrioritySystem::k_MaxPriority); // Always replicate controlled entities - // auto hierarchyController = controlledEnt.FindController(); - // if (hierarchyController != nullptr) - // { - // CollectControlledEntitiesRecursive(replicationSet, *hierarchyController); - // } - // } - //} + void ServerToClientReplicationWindow::UpdateHierarchyReplicationSet(ReplicationSet& replicationSet, NetworkHierarchyRootComponent& hierarchyComponent) + { + INetworkEntityManager* networkEntityManager = AZ::Interface::Get(); + AZ_Assert(networkEntityManager, "NetworkEntityManager must be created."); + + for (const AZ::Entity* controlledEntity : hierarchyComponent.m_hierarchicalEntities) + { + NetEntityId controlledNetEntitydId = networkEntityManager->GetNetEntityIdById(controlledEntity->GetId()); + AZ_Assert(controlledNetEntitydId != InvalidNetEntityId, "Unable to find the hierarchy entity in Network Entity Manager"); + + ConstNetworkEntityHandle controlledEntityHandle = networkEntityManager->GetEntity(controlledNetEntitydId); + AZ_Assert(controlledEntityHandle != nullptr, "We have lost a controlled entity unexpectedly"); + + replicationSet[controlledEntityHandle] = { NetEntityRole::Autonomous, 1.0f }; + } + } } diff --git a/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.h b/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.h index b034bde90c..8816209d7b 100644 --- a/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.h +++ b/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.h @@ -20,6 +20,7 @@ namespace Multiplayer { class NetSystemComponent; + class NetworkHierarchyRootComponent; class ServerToClientReplicationWindow : public IReplicationWindow @@ -56,7 +57,7 @@ namespace Multiplayer void OnEntityActivated(AZ::Entity* entity); void OnEntityDeactivated(AZ::Entity* entity); - //void CollectControlledEntitiesRecursive(ReplicationSet& replicationSet, EntityHierarchyComponent::Authority& hierarchyController); + void UpdateHierarchyReplicationSet(ReplicationSet& replicationSet, NetworkHierarchyRootComponent& hierarchyComponent); void EvaluateConnection(); void AddEntityToReplicationSet(ConstNetworkEntityHandle& entityHandle, float priority, float distanceSquared); diff --git a/Gems/Multiplayer/Code/Tests/AutoGen/TestMultiplayerComponent.AutoComponent.xml b/Gems/Multiplayer/Code/Tests/AutoGen/TestMultiplayerComponent.AutoComponent.xml new file mode 100644 index 0000000000..6b18e5ac70 --- /dev/null +++ b/Gems/Multiplayer/Code/Tests/AutoGen/TestMultiplayerComponent.AutoComponent.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp b/Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp index d6dd068c3f..badee73e04 100644 --- a/Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp +++ b/Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp @@ -16,6 +16,8 @@ #include #include #include +#include +#include namespace Multiplayer { @@ -175,10 +177,10 @@ namespace Multiplayer void CreateSimpleHierarchy(EntityInfo& root, EntityInfo& child) { PopulateHierarchicalEntity(root); - SetupEntity(root.m_entity, root.m_netId, NetEntityRole::Client); + SetupEntity(root.m_entity, root.m_netId, NetEntityRole::Autonomous); PopulateHierarchicalEntity(child); - SetupEntity(child.m_entity, child.m_netId, NetEntityRole::Client); + SetupEntity(child.m_entity, child.m_netId, NetEntityRole::Autonomous); // we need a parent-id value to be present in NetworkTransformComponent (which is in client mode and doesn't have a controller) SetParentIdOnNetworkTransform(child.m_entity, root.m_netId); @@ -330,7 +332,7 @@ namespace Multiplayer void CreateDeepHierarchyOnClient(EntityInfo& childOfChild) { PopulateHierarchicalEntity(childOfChild); - SetupEntity(childOfChild.m_entity, childOfChild.m_netId, NetEntityRole::Client); + SetupEntity(childOfChild.m_entity, childOfChild.m_netId, NetEntityRole::Autonomous); // we need a parent-id value to be present in NetworkTransformComponent (which is in client mode and doesn't have a controller) SetParentIdOnNetworkTransform(childOfChild.m_entity, m_childOfChild->m_netId); @@ -387,4 +389,66 @@ namespace Multiplayer ); } } + + TEST_F(ClientDeepHierarchyTests, CreateProcessInputTest) + { + using MultiplayerTest::TestMultiplayerComponent; + using MultiplayerTest::TestMultiplayerComponentController; + using MultiplayerTest::TestMultiplayerComponentNetworkInput; + + auto* rootNetBind = m_root->m_entity->FindComponent(); + + NetworkInputArray inputArray(rootNetBind->GetEntityHandle()); + NetworkInput& input = inputArray[0]; + + const float deltaTime = 0.16f; + rootNetBind->CreateInput(input, deltaTime); + + auto ValidateCreatedInput = [](const NetworkInput& input, const HierarchyTests::EntityInfo& entityInfo) + { + // Validate test input for the root entity's TestMultiplayerComponent + auto* testInput = input.FindComponentInput(); + EXPECT_NE(testInput, nullptr); + + auto* testMultiplayerComponent = entityInfo.m_entity->FindComponent(); + EXPECT_NE(testMultiplayerComponent, nullptr); + + EXPECT_EQ(testInput->m_ownerId, testMultiplayerComponent->GetId()); + }; + + // Validate root input + ValidateCreatedInput(input, *m_root); + + // Validate children input + { + NetworkHierarchyRootComponentNetworkInput* rootHierarchyInput = input.FindComponentInput(); + const AZStd::vector& childInputs = rootHierarchyInput->m_childInputs; + EXPECT_EQ(childInputs.size(), 2); + ValidateCreatedInput(childInputs[0].GetNetworkInput(), *m_child); + ValidateCreatedInput(childInputs[1].GetNetworkInput(), *m_childOfChild); + } + + // Test ProcessInput + { + AZStd::unordered_set inputProcessedEntities; + size_t processInputCallCounter = 0; + auto processInputCallback = [&inputProcessedEntities, &processInputCallCounter](NetEntityId netEntityId) + { + inputProcessedEntities.insert(netEntityId); + processInputCallCounter++; + }; + + // Set the callbacks for processing input. This allows us to inspect how many times the input was processed + // and which entity's controller was invoked. + m_root->m_entity->FindComponent()->m_processInputCallback = processInputCallback; + m_child->m_entity->FindComponent()->m_processInputCallback = processInputCallback; + m_childOfChild->m_entity->FindComponent()->m_processInputCallback = processInputCallback; + + rootNetBind->ProcessInput(input, deltaTime); + + EXPECT_EQ(processInputCallCounter, 3); + EXPECT_EQ(inputProcessedEntities, + AZStd::unordered_set({ m_root->m_netId, m_child->m_netId, m_childOfChild->m_netId })); + } + } } diff --git a/Gems/Multiplayer/Code/Tests/CommonHierarchySetup.h b/Gems/Multiplayer/Code/Tests/CommonHierarchySetup.h index d6f65918c0..38027c11a9 100644 --- a/Gems/Multiplayer/Code/Tests/CommonHierarchySetup.h +++ b/Gems/Multiplayer/Code/Tests/CommonHierarchySetup.h @@ -30,6 +30,7 @@ #include #include #include +#include namespace Multiplayer { @@ -93,6 +94,12 @@ namespace Multiplayer m_netTransformDescriptor.reset(NetworkTransformComponent::CreateDescriptor()); m_netTransformDescriptor->Reflect(m_serializeContext.get()); + m_testMultiplayerComponentDescriptor.reset(MultiplayerTest::TestMultiplayerComponent::CreateDescriptor()); + m_testMultiplayerComponentDescriptor->Reflect(m_serializeContext.get()); + + m_testInputDriverComponentDescriptor.reset(MultiplayerTest::TestInputDriverComponent::CreateDescriptor()); + m_testInputDriverComponentDescriptor->Reflect(m_serializeContext.get()); + m_mockMultiplayer = AZStd::make_unique>(); AZ::Interface::Register(m_mockMultiplayer.get()); @@ -103,6 +110,7 @@ namespace Multiplayer GetMultiplayer()->GetStats().ReserveComponentStats(Multiplayer::InvalidNetComponentId, 50, 0); m_mockNetworkEntityManager = AZStd::make_unique>(); + AZ::Interface::Register(m_mockNetworkEntityManager.get()); ON_CALL(*m_mockNetworkEntityManager, AddEntityToEntityMap(_, _)).WillByDefault(Invoke(this, &HierarchyTests::AddEntityToEntityMap)); ON_CALL(*m_mockNetworkEntityManager, GetEntity(_)).WillByDefault(Invoke(this, &HierarchyTests::GetEntity)); @@ -136,6 +144,7 @@ namespace Multiplayer m_multiplayerComponentRegistry = AZStd::make_unique(); ON_CALL(*m_mockNetworkEntityManager, GetMultiplayerComponentRegistry()).WillByDefault(Return(m_multiplayerComponentRegistry.get())); RegisterMultiplayerComponents(); + MultiplayerTest::RegisterMultiplayerComponents(); } void TearDown() override @@ -157,6 +166,7 @@ namespace Multiplayer AZ::Interface::Unregister(m_mockNetworkTime.get()); AZ::Interface::Unregister(m_mockTime.get()); + AZ::Interface::Unregister(m_mockNetworkEntityManager.get()); AZ::Interface::Unregister(m_mockMultiplayer.get()); AZ::Interface::Unregister(m_mockComponentApplicationRequests.get()); @@ -165,6 +175,8 @@ namespace Multiplayer m_mockNetworkEntityManager.reset(); m_mockMultiplayer.reset(); + m_testInputDriverComponentDescriptor.reset(); + m_testMultiplayerComponentDescriptor.reset(); m_transformDescriptor.reset(); m_netTransformDescriptor.reset(); m_hierarchyRootDescriptor.reset(); @@ -186,6 +198,8 @@ namespace Multiplayer AZStd::unique_ptr m_hierarchyRootDescriptor; AZStd::unique_ptr m_hierarchyChildDescriptor; AZStd::unique_ptr m_netTransformDescriptor; + AZStd::unique_ptr m_testMultiplayerComponentDescriptor; + AZStd::unique_ptr m_testInputDriverComponentDescriptor; AZStd::unique_ptr> m_mockMultiplayer; AZStd::unique_ptr m_mockNetworkEntityManager; @@ -394,6 +408,9 @@ namespace Multiplayer entityInfo.m_entity->CreateComponent(); entityInfo.m_entity->CreateComponent(); entityInfo.m_entity->CreateComponent(); + entityInfo.m_entity->CreateComponent(); + entityInfo.m_entity->CreateComponent(); + switch (entityInfo.m_role) { case EntityInfo::Role::Root: diff --git a/Gems/Multiplayer/Code/Tests/TestMultiplayerComponent.cpp b/Gems/Multiplayer/Code/Tests/TestMultiplayerComponent.cpp new file mode 100644 index 0000000000..ac723300a9 --- /dev/null +++ b/Gems/Multiplayer/Code/Tests/TestMultiplayerComponent.cpp @@ -0,0 +1,76 @@ +/* +* Copyright (c) Contributors to the Open 3D Engine Project. For complete copyright and license terms please see the LICENSE at the root of this distribution. +* +* SPDX-License-Identifier: Apache-2.0 OR MIT +* +*/ +#include + +#include + +namespace MultiplayerTest +{ + void TestInputDriverComponent::Reflect(AZ::ReflectContext* context) + { + AZ::SerializeContext* serializeContext = azrtti_cast(context); + if (serializeContext) + { + serializeContext->Class() + ->Version(1); + } + } + + void TestMultiplayerComponent::Reflect(AZ::ReflectContext* context) + { + AZ::SerializeContext* serializeContext = azrtti_cast(context); + if (serializeContext) + { + serializeContext->Class() + ->Version(1); + } + TestMultiplayerComponentBase::Reflect(context); + } + + void TestMultiplayerComponent::OnInit() + { + } + + void TestMultiplayerComponent::OnActivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + } + + void TestMultiplayerComponent::OnDeactivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + } + + TestMultiplayerComponentController::TestMultiplayerComponentController(TestMultiplayerComponent& parent) + : TestMultiplayerComponentControllerBase(parent) + { + } + + void TestMultiplayerComponentController::OnActivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + } + + void TestMultiplayerComponentController::OnDeactivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + } + + void TestMultiplayerComponentController::CreateInput(Multiplayer::NetworkInput& input, [[maybe_unused]] float deltaTime) + { + auto* networkInput = input.FindComponentInput(); + networkInput->m_ownerId = GetParent().GetId(); + } + + void TestMultiplayerComponentController::ProcessInput(Multiplayer::NetworkInput& input, [[maybe_unused]] float deltaTime) + { + auto& component = GetParent(); + [[maybe_unused]] auto* networkInput = input.FindComponentInput(); + AZ_Assert(networkInput->m_ownerId == component.GetId(), "Input Id doesn't match the owner component Id"); + + if (component.m_processInputCallback) + { + component.m_processInputCallback(GetNetEntityId()); + } + } +} diff --git a/Gems/Multiplayer/Code/Tests/TestMultiplayerComponent.h b/Gems/Multiplayer/Code/Tests/TestMultiplayerComponent.h new file mode 100644 index 0000000000..4cf0be2c2d --- /dev/null +++ b/Gems/Multiplayer/Code/Tests/TestMultiplayerComponent.h @@ -0,0 +1,61 @@ +/* +* Copyright (c) Contributors to the Open 3D Engine Project. For complete copyright and license terms please see the LICENSE at the root of this distribution. +* +* SPDX-License-Identifier: Apache-2.0 OR MIT +* +*/ +#pragma once + +#include + +namespace MultiplayerTest +{ + // Dummy class for satisfying "MultiplayerInputDriver" component dependency + class TestInputDriverComponent : public AZ::Component + { + public: + AZ_COMPONENT(TestInputDriverComponent, "{C3877905-3B61-45AE-A636-9845C3AAA39D}"); + + static void Reflect(AZ::ReflectContext* context); + + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + { + provided.emplace_back(AZ_CRC_CE("MultiplayerInputDriver")); + } + + void Activate() override {} + void Deactivate() override {} + }; + + // Test multiplayer component with ability to create and process network input + class TestMultiplayerComponent + : public TestMultiplayerComponentBase + { + public: + AZ_MULTIPLAYER_COMPONENT(MultiplayerTest::TestMultiplayerComponent, s_testMultiplayerComponentConcreteUuid, MultiplayerTest::TestMultiplayerComponentBase); + + static void Reflect(AZ::ReflectContext* context); + + void OnInit() override; + void OnActivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + void OnDeactivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + + AZStd::function m_processInputCallback; + }; + + // Multiplayer controller for the test component + class TestMultiplayerComponentController + : public TestMultiplayerComponentControllerBase + { + public: + TestMultiplayerComponentController(TestMultiplayerComponent& parent); + + //! TestMultiplayerComponentControllerBase + void OnActivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + void OnDeactivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + + //! MultiplayerController interface + void CreateInput(Multiplayer::NetworkInput& input, float deltaTime) override; + void ProcessInput(Multiplayer::NetworkInput& input, float deltaTime) override; + }; +} diff --git a/Gems/Multiplayer/Code/multiplayer_tests_files.cmake b/Gems/Multiplayer/Code/multiplayer_tests_files.cmake index 12c92ef5c1..adc391fe31 100644 --- a/Gems/Multiplayer/Code/multiplayer_tests_files.cmake +++ b/Gems/Multiplayer/Code/multiplayer_tests_files.cmake @@ -7,6 +7,12 @@ # set(FILES + Include/Multiplayer/AutoGen/AutoComponentTypes_Header.jinja + Include/Multiplayer/AutoGen/AutoComponentTypes_Source.jinja + Include/Multiplayer/AutoGen/AutoComponent_Common.jinja + Include/Multiplayer/AutoGen/AutoComponent_Header.jinja + Include/Multiplayer/AutoGen/AutoComponent_Source.jinja + Tests/AutoGen/TestMultiplayerComponent.AutoComponent.xml Tests/ClientHierarchyTests.cpp Tests/ServerHierarchyBenchmarks.cpp Tests/CommonHierarchySetup.h @@ -20,4 +26,6 @@ set(FILES Tests/RewindableContainerTests.cpp Tests/RewindableObjectTests.cpp Tests/ServerHierarchyTests.cpp + Tests/TestMultiplayerComponent.h + Tests/TestMultiplayerComponent.cpp )